# Superwall
Source: https://superwall.com/docs/android/sdk-reference/Superwall
The shared instance of Superwall that provides access to all SDK features.
You must call [`configure()`](/android/sdk-reference/configure) before accessing `Superwall.instance`, otherwise your app will crash.
## Purpose
Provides access to the configured Superwall instance after calling [`configure()`](/android/sdk-reference/configure).
## Signature
```kotlin
companion object {
val instance: Superwall
}
```
```java
// Java
public static Superwall getInstance()
```
## Parameters
This is a companion object property with no parameters.
## Returns / State
Returns the shared `Superwall` instance that was configured via [`configure()`](/android/sdk-reference/configure).
## Usage
Configure first (typically in Application class):
```kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Superwall.configure(
application = this,
apiKey = "pk_your_api_key"
)
}
}
```
Then access throughout your app:
```kotlin
Superwall.instance.register("feature_access") {
// Feature code here
}
```
Set user identity and attributes:
```kotlin
Superwall.instance.identify("user123")
Superwall.instance.setUserAttributes(mapOf(
"plan" to "premium",
"signUpDate" to System.currentTimeMillis()
))
```
Reset the user:
```kotlin
Superwall.instance.reset()
```
Avoid calling `Superwall.instance.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](/android/quickstart/user-management) for more guidance.
Set delegate:
```kotlin
Superwall.instance.delegate = this
```
Consume a purchase (2.6.2+):
```kotlin
// Using coroutines
lifecycleScope.launch {
val result = Superwall.instance.consume(purchaseToken)
result.fold(
onSuccess = { token ->
println("Purchase consumed: $token")
},
onFailure = { error ->
println("Failed to consume: ${error.message}")
}
)
}
// Using callback
Superwall.instance.consume(purchaseToken) { result ->
result.fold(
onSuccess = { token ->
println("Purchase consumed: $token")
},
onFailure = { error ->
println("Failed to consume: ${error.message}")
}
)
}
```
Show an alert over the current paywall (2.5.3+):
```kotlin
Superwall.instance.showAlert(
title = "Important Notice",
message = "Your subscription will renew soon",
actionTitle = "View Details",
closeActionTitle = "Dismiss",
action = {
// Handle action button tap
navigateToSubscriptionSettings()
},
onClose = {
// Handle close/dismiss
println("Alert dismissed")
}
)
```
Set integration attributes for analytics (2.5.3+):
```kotlin
import com.superwall.sdk.models.attribution.AttributionProvider
Superwall.instance.setIntegrationAttributes(
mapOf(
AttributionProvider.ADJUST to "adjust_user_id_123",
AttributionProvider.MIXPANEL to "mixpanel_distinct_id_456",
AttributionProvider.META to "meta_user_id_789"
)
)
```
Java usage:
```java
// Access the instance
Superwall.getInstance().register("feature_access", () -> {
// Feature code here
});
// Set user identity
Superwall.getInstance().identify("user123");
```
---
# PurchaseController
Source: https://superwall.com/docs/android/sdk-reference/PurchaseController
An interface for handling Superwall's subscription-related logic with your own purchase implementation.
**This interface is not required.** By default, Superwall handles all subscription-related logic automatically using Google Play Billing.
When implementing PurchaseController, you must manually update [`subscriptionStatus`](/android/sdk-reference/subscriptionStatus) whenever the user's entitlements change.
## Purpose
Use this interface only if you want complete control over purchase handling, such as when using RevenueCat or other third-party purchase frameworks.
## Signature
```kotlin
interface PurchaseController {
suspend fun purchase(
activity: Activity,
product: StoreProduct
): PurchaseResult
suspend fun restorePurchases(): RestorationResult
}
```
```java
// Java
public interface PurchaseController {
CompletableFuture purchase(
Activity activity,
StoreProduct product
);
CompletableFuture restorePurchases();
}
```
## Parameters
| Name | Type | Description | Required |
| ---------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| purchase | activity: Activity, product: StoreProduct | Called when user initiates purchasing. Implement your purchase logic here. Activity is needed for Google Play Billing. 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(Throwable)`, `.Cancelled`, or `.Pending`)
* `restorePurchases()` returns a `RestorationResult` (`.Restored` or `.Failed(Throwable?)`)
When using a PurchaseController, you must also manage [`subscriptionStatus`](/android/sdk-reference/subscriptionStatus) yourself.
## Usage
For implementation examples and detailed guidance, see [Using RevenueCat](/android/guides/using-revenuecat).
---
# PaywallOptions
Source: https://superwall.com/docs/android/sdk-reference/PaywallOptions
Configuration for paywall presentation and behavior in the Superwall Android SDK.
`PaywallOptions` is provided via the `paywalls` property on [`SuperwallOptions`](/android/sdk-reference/SuperwallOptions) and is passed to the SDK when you call [`configure`](/android/sdk-reference/configure).
## Purpose
Customize how paywalls look and behave, including preload behavior, alerts, dismissal, and haptics.
## Signature
```kotlin
class PaywallOptions {
var isHapticFeedbackEnabled: Boolean = true
class RestoreFailed {
var title: String = "No Subscription Found"
var message: String = "We couldn't find an active subscription for your account."
var closeButtonTitle: String = "Okay"
}
var restoreFailed: RestoreFailed = RestoreFailed()
var shouldShowPurchaseFailureAlert: Boolean = true
var shouldPreload: Boolean = true
var useCachedTemplates: Boolean = false
var automaticallyDismiss: Boolean = true
enum class TransactionBackgroundView { SPINNER }
var transactionBackgroundView: TransactionBackgroundView? = TransactionBackgroundView.SPINNER
var overrideProductsByName: Map = emptyMap()
var optimisticLoading: Boolean = false
var onBackPressed: ((PaywallInfo?) -> Boolean)? = null
}
```
## Parameters
| Name | Type | Description | Default | Required |
| ------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | -------- |
| isHapticFeedbackEnabled | Boolean | Enables haptic feedback when users purchase/restore, open links, or close the paywall. | 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 |
| shouldShowPurchaseFailureAlert | Boolean | Shows an alert after a purchase fails. Set to \`false\` if you handle failures via a \`PurchaseController\`. | true | no |
| shouldPreload | Boolean | Preloads and caches trigger paywalls and products during SDK initialization. Set to \`false\` for just-in-time loading. | true | no |
| useCachedTemplates | Boolean | Loads paywall template websites from disk when available. | false | no |
| automaticallyDismiss | Boolean | Automatically dismisses the paywall on successful purchase or restore. | true | no |
| transactionBackgroundView | TransactionBackgroundView? | View shown behind the system payment sheet during a transaction. Use \`null\` for no view. | .SPINNER | no |
| overrideProductsByName | Map\ | Overrides products on all paywalls using name→identifier mapping (e.g., \`"primary"\` → \`"com.example.premium\_monthly"\`). | | yes |
| optimisticLoading | Boolean | Hides shimmer optimistically. | false | no |
| onBackPressed | ((PaywallInfo?) -> Boolean)? | Callback invoked when back button is pressed (requires \`reroute\_back\_button\` enabled in paywall settings). Return \`true\` to consume the back press, \`false\` to use SDK default behavior. | null | no |
## Usage
```kotlin
val paywallOptions = PaywallOptions().apply {
isHapticFeedbackEnabled = true
shouldShowPurchaseFailureAlert = false
shouldPreload = true
useCachedTemplates = false
automaticallyDismiss = true
transactionBackgroundView = PaywallOptions.TransactionBackgroundView.SPINNER
overrideProductsByName = mapOf(
"primary" to "com.example.premium_monthly",
"tertiary" to "com.example.premium_annual",
)
optimisticLoading = false
onBackPressed = { paywallInfo ->
// Custom back button handling
// Return true to consume the back press, false to use SDK default
false
}
}
val options = SuperwallOptions().apply {
paywalls = paywallOptions
}
Superwall.configure(
application = this,
apiKey = "pk_your_api_key",
options = options,
)
```
## Related
* [`SuperwallOptions`](/android/sdk-reference/SuperwallOptions)
---
# register()
Source: https://superwall.com/docs/android/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
```kotlin
fun Superwall.register(
placement: String,
params: Map? = null,
handler: PaywallPresentationHandler? = null,
feature: () -> Unit,
)
```
```kotlin
fun Superwall.register(
placement: String,
params: Map? = null,
handler: PaywallPresentationHandler? = null,
)
```
## Parameters
| Name | Type | Description | Default | Required |
| --------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| placement | String | The name of the placement you wish to register. | | yes |
| params | Map\? | Optional parameters to pass with your placement. These can be referenced within campaign rules. Keys beginning with \`$\` are reserved for Superwall and will be dropped. Arrays and nested maps are currently unsupported and will be ignored. | null | no |
| handler | PaywallPresentationHandler? | A handler whose functions provide status updates for the paywall lifecycle. | null | no |
| feature | () -> Unit | A 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\*\*. | | yes |
## Returns / State
This function returns `Unit`. If you supply a `feature` lambda, it will be executed according to the paywall's gating configuration, as described above.
## Usage
```swift iOS
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()
}
}
```
```kotlin Android
fun pressedWorkoutButton() {
// remotely decide if a paywall is shown and if
// navigation.startWorkout() is a paid-only feature
Superwall.instance.register("StartWorkout") {
navigation.startWorkout()
}
}
```
```dart Flutter
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();
});
}
```
```typescript React Native
// remotely decide if a paywall is shown and if
// navigation.startWorkout() is a paid-only feature
Superwall.shared.register({
placement: 'StartWorkout',
feature: () => {
navigation.navigate('LaunchedFeature', {
value: 'Non-gated feature launched',
});
}
});
```
Register without feature gating:
```kotlin
Superwall.instance.register(
placement = "onboarding_complete",
params = mapOf("source" to "onboarding"),
handler = this
)
```
---
# subscriptionStatus
Source: https://superwall.com/docs/android/sdk-reference/subscriptionStatus
A StateFlow property that indicates the subscription status of the user.
If you're using a custom [`PurchaseController`](/android/sdk-reference/PurchaseController), you must update this property whenever the user's entitlements change.
You can also observe changes via the [`SuperwallDelegate`](/android/sdk-reference/SuperwallDelegate) method `subscriptionStatusDidChange(from, to)`.
## Purpose
Indicates the current subscription status of the user and can be observed for changes using Kotlin StateFlow.
## Signature
```kotlin
val subscriptionStatus: StateFlow
// For setting the status (when using custom PurchaseController)
fun setSubscriptionStatus(status: SubscriptionStatus)
```
```java
// Java
public StateFlow getSubscriptionStatus()
public void setSubscriptionStatus(SubscriptionStatus status)
```
## Parameters
This property accepts a `SubscriptionStatus` sealed class value:
* `SubscriptionStatus.Unknown` - Status is not yet determined
* `SubscriptionStatus.Active(Set)` - User has active entitlements (set of entitlement identifiers)
* `SubscriptionStatus.Inactive` - User has no active entitlements
## Returns / State
Returns a `StateFlow` that emits the current subscription status. When using a [`PurchaseController`](/android/sdk-reference/PurchaseController), you must set this property yourself using `setSubscriptionStatus()`. Otherwise, Superwall manages it automatically.
## Usage
Set subscription status (when using PurchaseController):
```kotlin
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(setOf("premium", "pro_features"))
)
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
```
Get current subscription status:
```kotlin
val status = Superwall.instance.subscriptionStatus.value
when (status) {
is SubscriptionStatus.Unknown ->
println("Subscription status unknown")
is SubscriptionStatus.Active ->
println("User has active entitlements: ${status.entitlements}")
is SubscriptionStatus.Inactive ->
println("User has no active subscription")
}
```
Observe changes with StateFlow:
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
Superwall.instance.subscriptionStatus.collect { status ->
updateUI(status)
}
}
}
private fun updateUI(status: SubscriptionStatus) {
when (status) {
is SubscriptionStatus.Active -> showPremiumContent()
is SubscriptionStatus.Inactive -> showFreeContent()
is SubscriptionStatus.Unknown -> showLoadingState()
}
}
}
```
Jetpack Compose observation:
```kotlin
@Composable
fun ContentScreen() {
val subscriptionStatus by Superwall.instance.subscriptionStatus
.collectAsState()
Column {
when (subscriptionStatus) {
is SubscriptionStatus.Active -> {
Text("Premium user with: ${subscriptionStatus.entitlements.joinToString()}")
}
is SubscriptionStatus.Inactive -> {
Text("Free user")
}
is SubscriptionStatus.Unknown -> {
Text("Loading...")
}
}
}
}
```
Java usage:
```java
// Get current status
SubscriptionStatus status = Superwall.getInstance()
.getSubscriptionStatus().getValue();
// Observe changes
Superwall.getInstance().getSubscriptionStatus()
.observe(this, status -> {
updateUI(status);
});
// Set status (when using PurchaseController)
Superwall.getInstance().setSubscriptionStatus(
new SubscriptionStatus.Active(Set.of("premium"))
);
```
---
# setSubscriptionStatus()
Source: https://superwall.com/docs/android/sdk-reference/advanced/setSubscriptionStatus
A function that manually sets the subscription status when using a custom PurchaseController.
This function should only be used when implementing a custom [`PurchaseController`](/android/sdk-reference/PurchaseController). When using Superwall's built-in purchase handling, the subscription status is managed automatically.
You must call this function whenever the user's entitlements change to keep Superwall's subscription status synchronized with your purchase system.
## Purpose
Manually updates the subscription status when using a custom [`PurchaseController`](/android/sdk-reference/PurchaseController) to ensure paywall gating and analytics work correctly.
## Signature
```kotlin
fun Superwall.setSubscriptionStatus(status: SubscriptionStatus)
```
```java
// Java
public void setSubscriptionStatus(SubscriptionStatus status)
```
## Parameters
| Name | Type | Description | Required |
| ------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| status | SubscriptionStatus | The subscription status to set. Can be \`SubscriptionStatus.Unknown\`, \`SubscriptionStatus.Active(entitlements)\`, or \`SubscriptionStatus.Inactive\`. | yes |
## Returns / State
This function returns `Unit`. The new status will be reflected in the [`subscriptionStatus`](/android/sdk-reference/subscriptionStatus) StateFlow and will trigger the [`SuperwallDelegate.subscriptionStatusDidChange`](/android/sdk-reference/SuperwallDelegate) callback.
## Usage
Set active subscription with entitlements:
```kotlin
// User purchased premium subscription
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(setOf("premium", "pro_features"))
)
```
Set inactive subscription:
```kotlin
// User's subscription expired or was cancelled
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
```
Set unknown status during initialization:
```kotlin
// While checking subscription status on app launch
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Unknown)
```
Usage with RevenueCat:
```kotlin
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)
}
}
}
```
Listen for external subscription changes:
```kotlin
class SubscriptionManager {
fun onSubscriptionStatusChanged(isActive: Boolean, entitlements: Set) {
val status = if (isActive) {
SubscriptionStatus.Active(entitlements)
} else {
SubscriptionStatus.Inactive
}
// Update Superwall whenever subscription status changes externally
Superwall.instance.setSubscriptionStatus(status)
}
}
```
Java usage:
```java
// Set active subscription
Set entitlements = Set.of("premium", "pro_features");
Superwall.getInstance().setSubscriptionStatus(
new SubscriptionStatus.Active(entitlements)
);
// Set inactive subscription
Superwall.getInstance().setSubscriptionStatus(
SubscriptionStatus.Inactive.INSTANCE
);
```
---
# PaywallBuilder
Source: https://superwall.com/docs/android/sdk-reference/advanced/PaywallBuilder
A builder class for creating custom PaywallView instances for advanced presentation.
You're responsible for managing the lifecycle of the returned PaywallView. Do not use the same PaywallView instance in multiple places simultaneously.
The remotely configured presentation style is ignored when using this method. You must handle presentation styling programmatically.
## Purpose
Creates a PaywallView that you can present however you want, bypassing Superwall's automatic presentation logic.
## Signature
```kotlin
class PaywallBuilder(private val placement: String) {
fun params(params: Map?): PaywallBuilder
fun overrides(overrides: PaywallOverrides?): PaywallBuilder
fun delegate(delegate: PaywallViewCallback): PaywallBuilder
fun activity(activity: Activity): PaywallBuilder
suspend fun build(): Result
fun buildSync(): PaywallView
fun build(onSuccess: (PaywallView) -> Unit, onError: (Throwable) -> Unit)
}
```
## Parameters
| Name | Type | Description | Required |
| --------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------- |
| placement | String | The name of the placement as defined on the Superwall dashboard. | yes |
| params | Map\? | Optional parameters to pass with your placement for audience filters. Keys beginning with \`$\` are reserved and will be dropped. | no |
| overrides | PaywallOverrides? | Optional overrides for products and presentation style. | no |
| delegate | PaywallViewCallback | A delegate to handle user interactions with the retrieved PaywallView. | yes |
| activity | Activity | The activity context required for the PaywallView. | yes |
## Returns / State
Returns a `Result` that you can add to your view hierarchy. If presentation should be skipped, returns a failure result.
## Usage
Using with coroutines:
```kotlin
lifecycleScope.launch {
val result = PaywallBuilder("premium_feature")
.params(mapOf("source" to "settings"))
.delegate(object : PaywallViewCallback {
override fun onFinished(
paywall: PaywallView,
result: PaywallResult,
shouldDismiss: Boolean
) {
// Handle paywall completion
}
})
.activity(this@MainActivity)
.build()
result.fold(
onSuccess = { paywallView ->
binding.container.addView(paywallView)
},
onFailure = { error ->
println("Error creating paywall: ${error.message}")
}
)
}
```
Jetpack Compose integration:
```kotlin
@Composable
fun PaywallScreen() {
PaywallComposable(
placement = "premium_feature",
params = mapOf("source" to "settings"),
delegate = object : PaywallViewCallback {
override fun onFinished(
paywall: PaywallView,
result: PaywallResult,
shouldDismiss: Boolean
) {
// Handle completion
}
},
errorComposable = { error ->
Text("Failed to load paywall: ${error.message}")
},
loadingComposable = {
CircularProgressIndicator()
}
)
}
```
---
# userId
Source: https://superwall.com/docs/android/sdk-reference/userId
A property on Superwall.instance 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()`](/android/sdk-reference/identify) or an anonymous ID if not identified.
## Signature
```kotlin
// Accessed via Superwall.instance
val userId: String
```
```java
// Java
public String getUserId()
```
## Parameters
This is a read-only property on the [`Superwall.instance`](/android/sdk-reference/Superwall) with no parameters.
## Returns / State
Returns a `String` representing the user's ID. If [`identify()`](/android/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:
```kotlin
val currentUserId = Superwall.instance.userId
println("User ID: $currentUserId")
```
Check if user is identified:
```kotlin
if (Superwall.instance.isLoggedIn) {
println("User is identified with ID: ${Superwall.instance.userId}")
} else {
println("User is anonymous with ID: ${Superwall.instance.userId}")
}
```
Example usage in analytics:
```kotlin
fun trackAnalyticsEvent() {
val userId = Superwall.instance.userId
Analytics.track("feature_used", mapOf(
"user_id" to userId,
"timestamp" to System.currentTimeMillis()
))
}
```
Example usage in custom logging:
```kotlin
fun logError(error: Throwable) {
Logger.log("Error for user ${Superwall.instance.userId}: ${error.message}")
}
```
Java usage:
```java
// Get current user ID
String currentUserId = Superwall.getInstance().getUserId();
System.out.println("User ID: " + currentUserId);
// Check if user is identified
if (Superwall.getInstance().isLoggedIn()) {
System.out.println("User is identified with ID: " +
Superwall.getInstance().getUserId());
} else {
System.out.println("User is anonymous with ID: " +
Superwall.getInstance().getUserId());
}
```
---
# identify()
Source: https://superwall.com/docs/android/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
```kotlin
fun Superwall.identify(
userId: String,
options: IdentityOptions? = null
)
```
```java
// Java
public void identify(
String userId,
@Nullable IdentityOptions options
)
```
## 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. | null | no |
## Returns / State
This function returns `Unit`. After calling, [`isLoggedIn`](/android/sdk-reference/userId) will return `true` and [`userId`](/android/sdk-reference/userId) will return the provided user ID.
## Usage
Basic identification:
```kotlin
Superwall.instance.identify("user_12345")
```
With options for account switching scenarios:
```kotlin
val options = IdentityOptions().apply {
restorePaywallAssignments = true
}
Superwall.instance.identify(
userId = "returning_user_67890",
options = options
)
```
Call as soon as you have a user ID:
```kotlin
fun userDidLogin(user: User) {
Superwall.instance.identify(user.id)
// Set additional user attributes
Superwall.instance.setUserAttributes(mapOf(
"email" to user.email,
"plan" to user.subscriptionPlan,
"signUpDate" to user.createdAt
))
}
```
Java usage:
```java
// Basic identification
Superwall.getInstance().identify("user_12345");
// With options
IdentityOptions options = new IdentityOptions();
options.setRestorePaywallAssignments(true);
Superwall.getInstance().identify("returning_user_67890", options);
```
---
# SuperwallEvent
Source: https://superwall.com/docs/android/sdk-reference/SuperwallEvent
A sealed class 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`](/android/sdk-reference/SuperwallDelegate) for forwarding to your analytics platform.
## Signature
```kotlin
sealed class SuperwallEvent {
// User lifecycle events
object FirstSeen : SuperwallEvent()
object AppOpen : SuperwallEvent()
object AppLaunch : SuperwallEvent()
object AppClose : SuperwallEvent()
object SessionStart : SuperwallEvent()
object IdentityAlias : SuperwallEvent()
object AppInstall : SuperwallEvent()
// Deep linking
data class DeepLink(val url: String) : SuperwallEvent()
// Paywall events
data class TriggerFire(
val placementName: String,
val result: TriggerResult
) : SuperwallEvent()
data class PaywallOpen(val paywallInfo: PaywallInfo) : SuperwallEvent()
data class PaywallClose(val paywallInfo: PaywallInfo) : SuperwallEvent()
data class PaywallDecline(val paywallInfo: PaywallInfo) : SuperwallEvent()
// Transaction events
data class TransactionStart(
val product: StoreProduct,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
data class TransactionComplete(
val transaction: StoreTransaction?,
val product: StoreProduct,
val type: TransactionType,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
data class TransactionFail(
val error: TransactionError,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
data class TransactionAbandon(
val product: StoreProduct,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
data class TransactionRestore(
val restoreType: RestoreType,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
data class TransactionTimeout(val paywallInfo: PaywallInfo) : SuperwallEvent()
// Subscription events
data class SubscriptionStart(
val product: StoreProduct,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
data class FreeTrialStart(
val product: StoreProduct,
val paywallInfo: PaywallInfo
) : SuperwallEvent()
object SubscriptionStatusDidChange : SuperwallEvent()
// System events
data class DeviceAttributes(val attributes: Map) : SuperwallEvent()
// And more...
}
```
```java
// Java - SuperwallEvent is a sealed class hierarchy
// Access via pattern matching or instanceof checks
```
## Parameters
Each event 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: String` - Deep link URLs
* `attributes: Map` - Device or user attributes
## Returns / State
This is a sealed class that represents different event types. Events are received via [`SuperwallDelegate.handleSuperwallEvent(eventInfo)`](/android/sdk-reference/SuperwallDelegate).
## Usage
These events are received via [`SuperwallDelegate.handleSuperwallEvent(eventInfo)`](/android/sdk-reference/SuperwallDelegate) for forwarding to your analytics platform.
---
# getPresentationResult()
Source: https://superwall.com/docs/android/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
```kotlin
suspend fun Superwall.getPresentationResult(
placement: String,
params: Map? = null,
): Result
```
```kotlin
fun Superwall.getPresentationResultSync(
placement: String,
params: Map? = null,
): Result
```
`getPresentationResultSync` blocks the calling thread. Prefer the suspend function inside a coroutine whenever possible.
## Parameters
| Name | Type | Description | Default | Required |
| --------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| placement | String | The placement to evaluate. | | yes |
| params | Map\? | Optional custom parameters that feed audience filters. Keys starting with \`$\` are dropped by the SDK. Nested maps or lists are ignored. | null | no |
## Returns / State
Returns a Kotlin `Result`.
* On success, the wrapped `PresentationResult` can be:
* `PresentationResult.PlacementNotFound` – Placement is missing from any live campaign.
* `PresentationResult.NoAudienceMatch` – No audience matched, so nothing would show.
* `PresentationResult.Paywall(experiment)` – A paywall would be presented; inspect the `experiment`.
* `PresentationResult.Holdout(experiment)` – The user is in a holdout group for that experiment.
* `PresentationResult.PaywallNotAvailable` – The SDK could not present (no activity, already showing, offline, etc.).
* On failure, the `Result` contains the thrown exception (for example, the SDK is not configured yet). Inspect it with `exceptionOrNull()` or `onFailure`.
## Usage
```kotlin
lifecycleScope.launch {
val result = Superwall.instance.getPresentationResult(
placement = "premium_feature",
params = mapOf("source" to "settings")
)
result
.onSuccess { presentation ->
when (presentation) {
is PresentationResult.Paywall -> {
logExperiment(presentation.experiment)
showLockedState()
}
is PresentationResult.Holdout -> showHoldoutBanner()
is PresentationResult.NoAudienceMatch -> unlockFeature()
is PresentationResult.PlacementNotFound -> Timber.w("Missing placement configuration")
is PresentationResult.PaywallNotAvailable -> showOfflineMessage()
}
}
.onFailure { error ->
Timber.e(error, "Unable to fetch presentation result")
}
}
```
```kotlin
// Blocking usage (for example, inside a worker)
val result = Superwall.instance.getPresentationResultSync("premium_feature")
val presentation = result.getOrNull() ?: return
```
## Related
* [`register()`](/android/sdk-reference/register) – Registers a placement and may present a paywall.
* [`SuperwallDelegate`](/android/sdk-reference/SuperwallDelegate) – Receive callbacks when paywalls are presented.
---
# configure()
Source: https://superwall.com/docs/android/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. The Android SDK requires an Application context for initialization.
## Purpose
Configures the shared instance of Superwall with your API key and optional configurations, making it ready for use throughout your Android app.
```kotlin Android
public fun configure(
applicationContext: Application,
apiKey: String,
purchaseController: PurchaseController? = null,
options: SuperwallOptions? = null,
activityProvider: ActivityProvider? = null,
completion: ((Result) -> Unit)? = null
)
```
## Parameters
| Name | Type | Description | Default | Required |
| ------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------- | -------- |
| applicationContext | Application | Your Android Application instance, required for SDK initialization and lifecycle management. | | yes |
| apiKey | String | Your Public API Key from the Superwall dashboard settings. | | yes |
| purchaseController | PurchaseController? | Optional object for handling all subscription-related logic yourself. If \`null\`, Superwall handles subscription logic. | null | no |
| options | SuperwallOptions? (see /android/sdk-reference/SuperwallOptions) | Optional configuration object for customizing paywall appearance and behavior. | null | no |
| activityProvider | ActivityProvider? | Optional provider that supplies the current Activity when needed by the SDK. | null | no |
| completion | ((Result\) -> Unit)? | Optional completion handler called when Superwall finishes configuring. Result contains success or failure. | null | no |
## Returns / State
Configures the Superwall instance which is accessible via [`Superwall.instance`](/android/sdk-reference/Superwall).
```kotlin Android
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Superwall.configure(
applicationContext = this,
apiKey = "pk_your_api_key"
)
}
}
```
With custom options:
```kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val options = SuperwallOptions().apply {
paywalls.shouldShowPurchaseFailureAlert = false
}
Superwall.configure(
applicationContext = this,
apiKey = "pk_your_api_key",
options = options
) {
println("Superwall configured successfully")
}
}
}
```
---
# handleDeepLink()
Source: https://superwall.com/docs/android/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`](/android/sdk-reference/SuperwallEvent) and sent to your [`SuperwallDelegate`](/android/sdk-reference/SuperwallDelegate).
## Purpose
Processes a deep link URL and triggers any associated paywall campaigns configured on the Superwall dashboard.
## Signature
```kotlin
fun Superwall.handleDeepLink(url: String)
```
```java
// Java
public void handleDeepLink(String url)
```
## Parameters
| Name | Type | Description | Required |
| ---- | ------ | -------------------------------------------------- | -------- |
| url | String | The deep link URL to process for paywall triggers. | yes |
## Returns / State
This function returns `Unit`. If the URL matches a campaign configured on the dashboard, it may trigger a paywall presentation.
## Usage
In your Activity:
```kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Handle deep link on app launch
intent?.let { handleIntent(it) }
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let { handleIntent(it) }
}
private fun handleIntent(intent: Intent) {
val action = intent.action
val data = intent.data
if (Intent.ACTION_VIEW == action && data != null) {
val url = data.toString()
// Handle the deep link with Superwall
Superwall.instance.handleDeepLink(url)
// Continue with your app's deep link handling
handleAppDeepLink(url)
}
}
}
```
In your manifest (declare your deep link schemes):
```xml
```
Handle deep links in different scenarios:
```kotlin
fun handleDeepLinkFromNotification(url: String) {
// Handle deep link from push notification
Superwall.instance.handleDeepLink(url)
navigateToContent(url)
}
fun handleDeepLinkFromWebView(url: String) {
// Handle deep link from web view or external browser
Superwall.instance.handleDeepLink(url)
processWebViewDeepLink(url)
}
```
Java usage:
```java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
handleIntent(intent);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
String action = intent.getAction();
Uri data = intent.getData();
if (Intent.ACTION_VIEW.equals(action) && data != null) {
String url = data.toString();
// Handle the deep link with Superwall
Superwall.getInstance().handleDeepLink(url);
// Continue with your app's deep link handling
handleAppDeepLink(url);
}
}
}
```
Example deep link campaign setup:
```kotlin
// When users click a deep link like "myapp://premium-feature"
// The dashboard campaign can be configured to:
// 1. Track the deepLink event
// 2. Show a paywall if user is not subscribed
// 3. Direct to premium content if user is subscribed
Superwall.instance.handleDeepLink("myapp://premium-feature?source=email")
```
---
# SuperwallDelegate
Source: https://superwall.com/docs/android/sdk-reference/SuperwallDelegate
An interface that handles Superwall lifecycle events and analytics.
Set the delegate using `Superwall.instance.delegate = this` to receive these callbacks. For Java, use `setJavaDelegate()` for better Java interop.
Use `handleSuperwallEvent(eventInfo)` 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
```kotlin
interface SuperwallDelegate {
fun subscriptionStatusDidChange(
from: SubscriptionStatus,
to: SubscriptionStatus
) {}
fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {}
fun handleCustomPaywallAction(name: String) {}
fun willDismissPaywall(paywallInfo: PaywallInfo) {}
fun willPresentPaywall(paywallInfo: PaywallInfo) {}
fun didDismissPaywall(paywallInfo: PaywallInfo) {}
fun didPresentPaywall(paywallInfo: PaywallInfo) {}
fun paywallWillOpenURL(url: String) {}
fun paywallWillOpenDeepLink(url: String) {}
fun handleLog(
level: LogLevel,
scope: LogScope,
message: String,
info: Map?,
error: Throwable?
) {}
fun userAttributesDidChange(newAttributes: Map) {}
}
```
```java
// Java - Use SuperwallDelegateJava for better Java interop
public interface SuperwallDelegateJava {
default void subscriptionStatusDidChange(
SubscriptionStatus from,
SubscriptionStatus to
) {}
default void handleSuperwallEvent(SuperwallEventInfo eventInfo) {}
default void handleCustomPaywallAction(String name) {}
// ... other methods
}
```
## Parameters
All methods are optional to implement. Key methods include:
| Name | Type | Description | Required |
| --------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | -------- |
| subscriptionStatusDidChange | from: SubscriptionStatus, to: 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 |
| userAttributesDidChange | newAttributes: Map\ | Called when user attributes change outside your app (for example via the \`Set Attribute\` paywall action). | yes |
## Returns / State
All delegate methods return `Unit`. They provide information about Superwall events and state changes.
## Usage
Basic delegate setup:
```kotlin
class MainActivity : AppCompatActivity(), SuperwallDelegate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Superwall.instance.delegate = this
}
}
```
Track subscription status changes:
```kotlin
override fun subscriptionStatusDidChange(
from: SubscriptionStatus,
to: SubscriptionStatus
) {
println("Subscription changed from $from to $to")
updateUI(to)
}
```
Forward analytics events:
```kotlin
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
when (val event = eventInfo.event) {
is SuperwallEvent.PaywallOpen -> {
Analytics.track("paywall_opened", mapOf(
"paywall_id" to event.paywallInfo.id,
"placement" to event.paywallInfo.placement
))
}
is SuperwallEvent.TransactionComplete -> {
Analytics.track("subscription_purchased", mapOf(
"product_id" to event.product.id,
"paywall_id" to event.paywallInfo.id
))
}
else -> {
// Handle other events
}
}
}
```
Handle custom paywall actions:
```kotlin
override fun handleCustomPaywallAction(name: String) {
when (name) {
"help" -> presentHelpScreen()
"contact" -> presentContactForm()
else -> println("Unknown custom action: $name")
}
}
```
Handle paywall lifecycle:
```kotlin
override fun willPresentPaywall(paywallInfo: PaywallInfo) {
// Pause video, hide UI, etc.
pauseBackgroundTasks()
}
override fun didDismissPaywall(paywallInfo: PaywallInfo) {
// Resume video, show UI, etc.
resumeBackgroundTasks()
}
```
Handle user attributes changes:
```kotlin
override fun userAttributesDidChange(newAttributes: Map) {
// React to paywall-triggered attribute updates
println("User attributes updated: $newAttributes")
refreshProfileUI(newAttributes)
}
```
Java usage:
```java
public class MainActivity extends AppCompatActivity implements SuperwallDelegateJava {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Superwall.getInstance().setJavaDelegate(this);
}
@Override
public void subscriptionStatusDidChange(
SubscriptionStatus from,
SubscriptionStatus to
) {
System.out.println("Subscription changed from " + from + " to " + to);
updateUI(to);
}
}
```
---
# setUserAttributes()
Source: https://superwall.com/docs/android/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 nested maps 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
```kotlin
fun Superwall.setUserAttributes(attributes: Map)
```
```java
// Java
public void setUserAttributes(Map attributes)
```
## Parameters
| Name | Type | Description | Required |
| ---------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| attributes | Map\ | A map of custom attributes to store for the user. Values can be any JSON encodable value, including strings, numbers, booleans, URLs, or timestamps. | yes |
## Returns / State
This function returns `Unit`. If an attribute already exists, its value will be overwritten while other attributes remain unchanged.
## Usage
Set multiple user attributes:
```kotlin
val attributes = mapOf(
"name" to "John Doe",
"email" to "john@example.com",
"plan" to "premium",
"signUpDate" to System.currentTimeMillis(),
"profilePicUrl" to "https://example.com/pic.jpg",
"isVip" to true,
"loginCount" to 42
)
Superwall.instance.setUserAttributes(attributes)
```
Set individual attributes over time:
```kotlin
Superwall.instance.setUserAttributes(mapOf("lastActiveDate" to System.currentTimeMillis()))
Superwall.instance.setUserAttributes(mapOf("featureUsageCount" to 15))
```
Remove an attribute by setting it to null:
```kotlin
Superwall.instance.setUserAttributes(mapOf("temporaryFlag" to null))
```
Real-world example after user updates profile:
```kotlin
fun updateUserProfile(user: User) {
Superwall.instance.setUserAttributes(mapOf(
"name" to user.displayName,
"avatar" to user.avatarURL,
"preferences" to user.notificationPreferences,
"lastUpdated" to System.currentTimeMillis()
))
}
```
Java usage:
```java
// Set multiple attributes
Map attributes = new HashMap<>();
attributes.put("name", "John Doe");
attributes.put("email", "john@example.com");
attributes.put("plan", "premium");
attributes.put("loginCount", 42);
Superwall.getInstance().setUserAttributes(attributes);
```
---
# SuperwallOptions
Source: https://superwall.com/docs/android/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, purchasing behavior, and Google Play settings for each environment.
## Purpose
Configures paywall presentation, logging, Google Play purchase behavior, and other global SDK settings. Pass an instance in to [`Superwall.configure`](/android/sdk-reference/configure).
## Signature
```kotlin
class SuperwallOptions {
var paywalls: PaywallOptions = PaywallOptions()
var shouldObservePurchases: Boolean = false
var networkEnvironment: NetworkEnvironment = NetworkEnvironment.Release()
var isExternalDataCollectionEnabled: Boolean = true
var localeIdentifier: String? = null
var isGameControllerEnabled: Boolean = false
var passIdentifiersToPlayStore: Boolean = false
var enableExperimentalDeviceVariables: Boolean = false
var logging: Logging = Logging()
var useMockReviews: Boolean = false
}
```
```java
// Java
public class SuperwallOptions {
public PaywallOptions paywalls = new PaywallOptions();
public boolean shouldObservePurchases = false;
public NetworkEnvironment networkEnvironment = new NetworkEnvironment.Release();
public boolean isExternalDataCollectionEnabled = true;
public @Nullable String localeIdentifier = null;
public boolean isGameControllerEnabled = false;
public boolean passIdentifiersToPlayStore = false;
public boolean enableExperimentalDeviceVariables = false;
public Logging logging = new Logging();
public boolean useMockReviews = false;
}
```
## Parameters
| Name | Type | Description | Default | Required |
| --------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------- | -------- |
| paywalls | PaywallOptions (see /android/sdk-reference/PaywallOptions) | Controls paywall presentation, preload, and alert behavior. | | yes |
| shouldObservePurchases | Boolean | Set to \`true\` to have Superwall observe Google Play purchases you make outside the SDK. | | yes |
| networkEnvironment | NetworkEnvironment | Overrides the API environment. \*\*Only change if instructed by Superwall.\*\* | | yes |
| isExternalDataCollectionEnabled | Boolean | Allows Superwall to send non-paywall analytics events to the backend. | true | no |
| localeIdentifier | String? | Overrides the locale used for rule evaluation (e.g., \`"en\_GB"\`). | | no |
| isGameControllerEnabled | Boolean | Forwards game controller events to paywalls. | | yes |
| passIdentifiersToPlayStore | Boolean | When \`true\`, Google Play receives the raw \`userId\` as \`obfuscatedExternalAccountId\`; otherwise Superwall sends a SHA-256 hash. | | yes |
| enableExperimentalDeviceVariables | Boolean | Enables experimental device variables (subject to change). | | yes |
| logging | Logging | Sets log level and scopes printed to Logcat. | | yes |
| useMockReviews | Boolean | Shows mock Google Play review dialogs for testing. | | yes |
## How `passIdentifiersToPlayStore` affects Google Play
Superwall always calls `BillingFlowParams.Builder.setObfuscatedAccountId(Superwall.instance.externalAccountId)` when launching a billing flow.
* **Default (`false`)** – `externalAccountId` is a SHA-256 hash of the `userId`. Google Play displays the hashed value as `obfuscatedExternalAccountId`, and the same hash is sent back to your servers.
* **Enabled (`true`)** – Superwall forwards the exact `appUserId` you passed to [`identify()`](/android/sdk-reference/identify). This makes it easier to correlate Google Play purchases with your users, but the value must comply with [Google's policy](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId) and must not contain PII.
Configure it as part of your application startup:
```kotlin
val options = SuperwallOptions().apply {
passIdentifiersToPlayStore = true
logging.level = LogLevel.info
}
Superwall.configure(
application = this,
apiKey = "pk_your_api_key",
options = options
)
```
## Related
* [`PaywallOptions`](/android/sdk-reference/PaywallOptions)
---
# Overview
Source: https://superwall.com/docs/android/sdk-reference
Reference documentation for the Superwall Android SDK.
## Welcome to the Superwall Android SDK Reference
You can find the source code for the SDK [on GitHub](https://github.com/superwall/Superwall-Android) along with our [example app](https://github.com/superwall/Superwall-Android/tree/develop/example).
## 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-android/issues).
---
# Tracking Subscription State
Source: https://superwall.com/docs/android/quickstart/tracking-subscription-state
Here's how to view whether or not a user is on a paid plan in Android.
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:
```kotlin
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)` - 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:
```kotlin
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:
```kotlin
@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:
```kotlin
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:
```kotlin
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:
```kotlin
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`:
```kotlin
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](/campaigns-audience#matching-to-entitlements) for determining when to show paywalls. You generally don't need to wrap your calls to register placements with subscription status checks:
```kotlin
// ❌ 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](/campaigns-audience#matching-to-entitlements), 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
Source: https://superwall.com/docs/android/quickstart/setting-user-properties
undefined
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(_:)`:
:::android
```kotlin Kotlin
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](/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/android/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
:::android
```kotlin Kotlin
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
:::android
```kotlin Kotlin
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](/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**.
:::android
```kotlin Kotlin
// 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](/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:
### 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:
---
# Configure the SDK
Source: https://superwall.com/docs/android/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**:

### 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:
:::android
```kotlin Kotlin
// 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](/advanced-configuration) for more.
You've now configured Superwall!
:::android
For further help, check out our [Android example apps](https://github.com/superwall/Superwall-Android/tree/master/Examples) for working examples of implementing the Superwall SDK.
:::
---
# User Management
Source: https://superwall.com/docs/android/quickstart/user-management
undefined
### 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.
:::android
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](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId)—it must not contain personally identifiable information.
:::
:::android
```kotlin Kotlin
// 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`](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.
```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/android/quickstart/install
Install the Superwall Android SDK via Gradle.
## Overview
To see the latest release, [check out the repository](https://github.com/superwall/Superwall-Android)
## Install via Gradle
[Gradle](https://developer.android.com/build/releases/gradle-plugin) 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](https://github.com/superwall/Superwall-Android/releases).

```groovy build.gradle
implementation "com.superwall.sdk:superwall-android:2.6.5"
```
```kotlin build.gradle.kts
implementation("com.superwall.sdk:superwall-android:2.6.5")
```
```toml libs.version.toml
[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:

```xml AndroidManifest.xml
```
Then add our Activity to your `AndroidManifest.xml`:

```xml AndroidManifest.xml
```
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 👇
---
# Handling Deep Links
Source: https://superwall.com/docs/android/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
:::android
The way to deep link into your app is URL Schemes.
:::
### Adding a Custom URL Scheme
:::android
Add the following to your `AndroidManifest.xml` file:
```xml
```
This configuration allows your app to open in response to a deep link with the format `exampleapp://` from your `MainActivity` class.
:::
### Handling Deep Links
:::android
In your `MainActivity` (or the activity specified in your intent-filter), add the following Kotlin code to handle deep links:
```kotlin Kotlin
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
### Handling App Links
## Adding app links
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 -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 `` tag declaring your deep link handling activity, add the following, replacing the domain with the one from the settings:
```xml
```
### 3. Handle incoming deep links using Superwall SDK
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.
```kotlin
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)
}
}
```
### 4. Handling links while testing
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](https://developer.android.com/training/app-links/verify-android-applinks).
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`
:::android
:::
::::
## 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](/presenting-paywalls-from-one-another) for examples of both.
---
# Post-Checkout Redirecting
Source: https://superwall.com/docs/android/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 a browser 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.
```kotlin
class SWDelegate : SuperwallDelegate {
override fun willRedeemLink() {
// Show a loading indicator to the user
showToast("Activating your purchase...")
}
}
```
You can manually dismiss the paywall at this point if needed, but note that 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 result can be one of the following:
* `RedemptionResult.Success`: The redemption succeeded and contains information about the redeemed code.
* `RedemptionResult.Error`: An error occurred while redeeming. You can check the error message via the error parameter.
* `RedemptionResult.ExpiredCode`: The code expired and contains information about whether a redemption email has been resent and an optional obfuscated email address.
* `RedemptionResult.InvalidCode`: The code that was redeemed was invalid.
* `RedemptionResult.ExpiredSubscription`: 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.
```kotlin
class SWDelegate : SuperwallDelegate {
override fun didRedeemLink(result: RedemptionResult) {
when (result) {
is RedemptionResult.ExpiredCode -> {
showToast("Expired Link")
Log.d("Superwall", "Code expired: ${result.code}, ${result.expiredInfo}")
}
is RedemptionResult.Error -> {
showToast(result.error.message)
Log.d("Superwall", "Error: ${result.code}, ${result.error}")
}
is RedemptionResult.ExpiredSubscription -> {
showToast("Expired Subscription")
Log.d("Superwall", "Expired subscription: ${result.code}, ${result.redemptionInfo}")
}
is RedemptionResult.InvalidCode -> {
showToast("Invalid Link")
Log.d("Superwall", "Invalid code: ${result.code}")
}
is RedemptionResult.Success -> {
val email = result.redemptionInfo.purchaserInfo.email
val productIdentifier = result.redemptionInfo.paywallInfo?.productIdentifier
if (email != null) {
Superwall.instance.setUserAttributes(mapOf("email" to email))
showToast("Welcome, $email!")
} else {
showToast("Welcome!")
}
// Access the product identifier if available (2.6.3+)
productIdentifier?.let {
Log.d("Superwall", "Redeemed product: $it")
}
}
}
}
}
```
---
# Redeeming In-App
Source: https://superwall.com/docs/android/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 Google Play Billing 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:
```kotlin
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.QueryPurchasesParams
import com.superwall.sdk.Superwall
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
suspend fun syncSubscriptionStatus(billingClient: BillingClient) {
withContext(Dispatchers.IO) {
val productIds = mutableSetOf()
// Get the device entitlements from Google Play Billing
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
val purchasesResult = billingClient.queryPurchasesAsync(params)
// Collect purchased product IDs
purchasesResult.purchasesList.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
purchase.products.forEach { productId ->
productIds.add(productId)
}
}
}
// Get products from Superwall and extract their entitlements
val storeProducts = Superwall.instance.getProducts(productIds)
val deviceEntitlements = storeProducts.flatMap { it.entitlements }.toSet()
// Get the web entitlements from Superwall
val webEntitlements = Superwall.instance.entitlements.web
// Merge the two sets of entitlements
val allEntitlements = deviceEntitlements + webEntitlements
// Update subscription status on the main thread
withContext(Dispatchers.Main) {
if (allEntitlements.isNotEmpty()) {
Superwall.instance.setSubscriptionStatus(
SubscriptionStatus.Active(allEntitlements)
)
} else {
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive)
}
}
}
}
```
In addition to syncing the subscription status when purchasing and restoring, you'll need to sync it whenever `didRedeemLink(result:)` is called:
```kotlin
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.models.redemption.RedemptionResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class SWDelegate(private val billingClient: BillingClient) : SuperwallDelegate {
private val coroutineScope = CoroutineScope(Dispatchers.Main)
override fun didRedeemLink(result: RedemptionResult) {
coroutineScope.launch {
syncSubscriptionStatus(billingClient)
}
}
}
```
### 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/android/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:
```kotlin
import com.revenuecat.purchases.Purchases
import com.superwall.sdk.Superwall
import com.superwall.sdk.delegate.SuperwallDelegate
import com.superwall.sdk.models.redemption.RedemptionResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
class SWDelegate : SuperwallDelegate {
private val client = OkHttpClient()
private val coroutineScope = CoroutineScope(Dispatchers.IO)
// The user tapped on a deep link to redeem a code
override fun willRedeemLink() {
Log.d("Superwall", "[!] willRedeemLink")
// Optionally show a loading indicator here
}
// Superwall received a redemption result and validated the purchase with Stripe.
override fun didRedeemLink(result: RedemptionResult) {
Log.d("Superwall", "[!] didRedeemLink: $result")
// Send Stripe IDs to RevenueCat to link purchases to the customer
// Get a list of subscription ids tied to the customer.
val stripeSubscriptionIds = when (result) {
is RedemptionResult.Success -> result.stripeSubscriptionIds
else -> null
} ?: return
val revenueCatStripePublicAPIKey = "strp....." // replace with your RevenueCat Stripe Public API Key
val appUserId = Purchases.sharedInstance.appUserID
// In the background using coroutines...
coroutineScope.launch {
// For each subscription id, link it to the user in RevenueCat
stripeSubscriptionIds.forEach { stripeSubscriptionId ->
try {
val json = JSONObject().apply {
put("app_user_id", appUserId)
put("fetch_token", stripeSubscriptionId)
}
val requestBody = json.toString()
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://api.revenuecat.com/v1/receipts")
.post(requestBody)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("X-Platform", "stripe")
.addHeader("Authorization", "Bearer $revenueCatStripePublicAPIKey")
.build()
client.newCall(request).execute().use { response ->
val responseBody = response.body?.string().orEmpty()
if (!response.isSuccessful) {
throw IOException("RevenueCat responded with ${response.code}: $responseBody")
}
Log.d("Superwall", "[!] Success: linked $stripeSubscriptionId to user $appUserId: $responseBody")
}
} catch (e: Exception) {
Log.e("Superwall", "[!] Error: unable to link $stripeSubscriptionId to user $appUserId", e)
}
}
/// After all network calls complete, invalidate the cache
Purchases.sharedInstance.getCustomerInfo(
onSuccess = { customerInfo ->
/// If you're using RevenueCat's `UpdatedCustomerInfoListener`, or keeping Superwall Entitlements in sync
/// via RevenueCat's listener methods, you don't need to do anything here. Those methods will be
/// called automatically when this call fetches the most up to date customer info, ignoring any local caches.
/// Otherwise, if you're manually calling `Purchases.sharedInstance.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
/// val webEntitlements = Superwall.instance.entitlements.web
},
onError = { error ->
Log.e("Superwall", "Error getting customer info", error)
}
)
// After all network calls complete, update UI on the main thread
withContext(Dispatchers.Main) {
// Perform UI updates on the main thread, like letting the user know their subscription was redeemed
}
}
}
}
```
The example surfaces non-200 responses and network exceptions so you can add retries, user messaging,
or monitoring. Customize the error handling to fit your production logging and UX.
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/android/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 v1 to v2 - Android
Source: https://superwall.com/docs/android/guides/migrations/migrating-to-v2
SuperwallKit 2.0 is a major release of Superwall's Android SDK. This introduces breaking changes.
## Migration steps
## 1. Update code references
### 1.1 Rename references from `event` to `placement`
In some most cases, the updates are simple and consist of renaming `events` to `placements` where necessary, for others, you'll need to run through this list to manually update your code.
| Before | After |
| ------------------------------------ | ---------------------------------------- |
| fun register(event:) | fun register(placement:) |
| fun preloadPaywalls(forEvents:) | fun preloadPaywalls(forPlacements:) |
| fun getPaywall(forEvent:) | fun getPaywall(forPlacement:) |
| fun getPresentationResult(forEvent:) | fun getPresentationResult(forPlacement:) |
| TriggerResult.EventNotFound | TriggerResult.PlacementNotFound |
| TriggerResult.NoRuleMatch | TriggerResult.NoAudienceMatch |
### 1.2 If using Compose and Paywall Composable
The `PaywallComposable` has been removed from the main Superwall SDK and moved into an optional `superwall-compose` library.
To use it, besides including the main superwall library, you'll now need to also include a `superwall-compose` artifact
```groovy gradle
implementation "com.superwall.sdk:superwall-compose:2.6.5"
```
## 2. Replacing getPaywall with PaywallBuilder
The default method for retrieving a paywall to display it yourself is changing to use the Builder pattern, allowing you more flexibility
in both retrieving and displaying paywalls. The builder allows for improved customisation of your paywall experience, allowing you to pass
in both custom Shimmer and Loading views by implementing proper interfaces.
Usage example:
```kotlin Android
val paywallView = PaywallBuilder("placement_name")
.params(mapOf("key" to "value"))
.overrides(PaywallOverrides())
.delegate(mySuperwallDelegate)
.shimmerView(MyShimmerView(context))
.purchaseLoadingView(MyPurchaseLoadingView(context))
.activity(activity)
.build()
```
### 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` with the `subscriptionStatus`:
| Before | After |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Superwall.shared.setSubscriptionStatus(SubscriptionStatus.ACTIVE) | Superwall.shared.setSubscriptionStatus(SubscriptionStatus.Active(entitlements)) |
You can get the `ProductDetails` 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:
```kotlin With Play Billing
suspend fun syncSubscriptionStatus() {
// We await for configuration to be set so our entitlements are available
Superwall.instance.configurationStateListener.first { it is ConfigurationStatus.Configured }
// Query purchases from your BillingClient
val subscriptionPurchases = queryPurchasesOfType(BillingClient.ProductType.SUBS)
val inAppPurchases = queryPurchasesOfType(BillingClient.ProductType.INAPP)
val allPurchases = subscriptionPurchases + inAppPurchases
val hasActivePurchaseOrSubscription =
allPurchases.any { it.purchaseState == Purchase.PurchaseState.PURCHASED }
val status: SubscriptionStatus =
if (hasActivePurchaseOrSubscription) {
subscriptionPurchases
.flatMap {
// Extract the productId
it.products
}.toSet() // Ensure uniqueness
.flatMap {
// Receive entitlements
val res = entitlementsInfo.byProductId(it)
res
}.toSet()
.let { entitlements ->
if (entitlements.isNotEmpty()) {
SubscriptionStatus.Active(entitlements)
} else {
SubscriptionStatus.Inactive
}
}
} else {
SubscriptionStatus.Inactive
}
Superwall.instance.setSubscriptionStatus(status)
}
```
```kotlin with RevenueCat
fun syncSubscriptionStatus() {
Purchases.sharedInstance.getCustomerInfoWith {
if (hasAnyActiveEntitlements(it)) {
setSubscriptionStatus(
SubscriptionStatus.Active(
it.entitlements.active
.map {
Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
}.toSet(),
),
)
} else {
setSubscriptionStatus(SubscriptionStatus.Inactive)
}
}
}
```
You can listen to the flowable property `Superwall.instance.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 v2 of the SDK, this is replaced with a check on the entitlements within the audience filter. As you migrate your users from v1 to v2 of the
SDK, you'll need to make sure you set both the entitlements check and the paywall presentation condition in the paywall editor.

## 6. Check out the full change log
You can view this on [our GitHub page](https://github.com/superwall/Superwall-Android/blob/develop/CHANGELOG.md).
## 7. Check out our updated example apps
All of our [example apps](https://github.com/superwall/Superwall-Android/tree/develop/example) 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 has multiple flavors, showing you how to use entitlements within your app as well as optionally using a purchase controller with Play Billing or RevenueCat.
---
# Using the Presentation Handler
Source: https://superwall.com/docs/android/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/android/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:

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:
:::android
```kotlin Android (Automatic)
val options = SuperwallOptions()
options.shouldObservePurchases = true
Superwall.configure(this, "your_api_key", options = options)
// In your purchase methods, replace Google's billing code with Superwall's proxy
val productDetailsParams =
SuperwallBillingFlowParams.ProductDetailsParams
.newBuilder()
.setOfferToken("test_offer_token")
.setProductDetails(mockProductDetails)
.build()
val params =
SuperwallBillingFlowParams
.newBuilder()
.setIsOfferPersonalized(true)
.setObfuscatedAccountId(...)
.setObfuscatedProfileId(...)
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
billingClient.launchBillingFlowWithSuperwall()
```
```kotlin Android (Manual)
val options = SuperwallOptions()
options.shouldObservePurchases = true
Superwall.configure(this, "your_api_key", options = options)
// In your purchase methods, begin by tracking the purchase start
Superwall.instance.observePurchaseStart(productDetails)
// On succesful purchase
Superwall.instance.observePurchaseResult(billingResult, purchases)
// On purchase error
Superwall.instance.observePurchaseError(productDetails, error)
```
:::
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/android/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()
}
}
}
}
}
```
---
# Custom Paywall Actions
Source: https://superwall.com/docs/android/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`:
:::android
```kotlin
override fun handleCustomPaywallAction(name: String) {
if (name == "help_center") {
HelpCenterManager.present()
}
}
```
:::
Remember to set `Superwall.shared.delegate`! For implementation details, see the [Superwall Delegate](/using-superwall-delegate) guide.
---
# Device Tier Targeting
Source: https://superwall.com/docs/android/guides/advanced/using-device-tiers-on-android
Target users based on their device performance capabilities to optimize paywall experiences across different hardware tiers.
The `deviceTier` variable allows you to create targeted audiences based on device performance capabilities. This helps optimize paywall experiences by showing resource-appropriate content to different device types. You can reference this in campaign filters, dynamic values, or in paywall text via the `device.deviceTier` variable.
Device tier targeting is available starting in Android SDK version `2.2.3`. Make sure you're using this version or later to access this feature.
## How device tier works
Device tier classification is based on several hardware factors:
* CPU performance
* Available RAM
* 4K/2K codec support
* Display quality
This automatic classification helps you deliver paywalls that perform well across the full spectrum of Android devices.
### Matching device ranges
When creating device tier filters, you can use `contains` or `equals` for narrower matching:
* **`contains`** - Broader matching that includes partial matches. For example, `deviceTier contains high` matches both `high` and `ultra_high` devices.
* **`equals`** - Exact matching for precise targeting. For example, `deviceTier equals high` matches only `high` tier devices, not `ultra_high`.
Use `contains` when you want to target a range of similar device capabilities, and `equals` when you need precise control over which specific tier to target.
## Device tier values
The `device.deviceTier` attribute returns one of these values:
* **`ultraLow`** - Entry-level devices with limited resources.
* **`low`** - Budget devices with basic performance capabilities.
* **`mid`** - Mid-range devices with moderate performance.
* **`high`** - Premium devices with strong performance.
* **`ultra_high`** - Flagship devices with top-tier specifications.
* **`unknown`** - Device tier couldn't be determined.
## Creating device tier audiences
To target users by device tier, create an audience using the `device.deviceTier` attribute:
1. Navigate to **Campaigns** in your dashboard.
2. Click on the campaign you want to target.
3. Edit, or create, a new audience.
4. Add a filter where `device.deviceTier` contains your target tier(s).
5. Save your audience.
You can target multiple tiers in a single audience. For example, use `deviceTier contains LOW` to target both `ultraLow` and `low` tier devices.
### Optimizing for lower-end devices
Create lightweight paywalls for devices that may struggle with resource-intensive content:
```
device.deviceTier contains ultraLow OR device.deviceTier contains low
```
Show these users paywalls with:
* Static images instead of videos.
* Compressed media files.
* Simplified animations.
---
# Purchasing Products Outside of a Paywall
Source: https://superwall.com/docs/android/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:
:::android
```kotlin Android
// Purchase product with first available baseplan
Superwall.instance.purchase("my_product_id")
// Purchase product with base plan and cheapest/free offer
Superwall.instance.purchase("my_product_id:base_plan:sw-auto")
// Purchase product with a specified offer
Superwall.instance.purchase("my_product_id:base_plan:offer")
// Purchase product with no offer
Superwall.instance.purchase("my_product_id:base_plan:sw-none")
```
:::
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:
:::android
```kotlin Android
val products = Superwall.instance.getProducts("id1", "id2")
```
:::
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/android/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.
---
# Game Controller Support
Source: https://superwall.com/docs/android/guides/advanced/game-controller-support
undefined
:::android
First set the `SuperwallOption` `isGameControllerEnabled` to `true`:
```kotlin
Superwall.instance.options.isGameControllerEnabled = true
```
Then Superwall will automatically listen for gamepad events and forward them to your paywall!
:::
:::flutter
First set the `SuperwallOption` `isGameControllerEnabled` to `true`:
```dart
Superwall.instance.options.isGameControllerEnabled = true
```
Then Superwall will automatically listen for gamepad events and forward them to your paywall!
:::
:::expo
First set the `SuperwallOption` `isGameControllerEnabled` to `true`:
```typescript
Superwall.instance.options.isGameControllerEnabled = true
```
Then Superwall will automatically listen for gamepad events and forward them to your paywall!
:::
---
# Experimental Flags
Source: https://superwall.com/docs/android/guides/experimental-flags
undefined
Support for Experimental Flags in Android is not yet available.
---
# Advanced Purchasing
Source: https://superwall.com/docs/android/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.
:::android
```kotlin Kotlin
// MyPurchaseController.kt
class MyPurchaseController(val context: Context): PurchaseController {
// 1
override suspend fun purchase(
activity: Activity,
productDetails: ProductDetails,
basePlanId: String?,
offerId: String?
): PurchaseResult {
// TODO
// ----
// Purchase via GoogleBilling, RevenueCat, Qonversion or however
// you like and return a valid PurchaseResult
return PurchaseResult.Purchased()
}
// 2
override suspend fun restorePurchases(): RestorationResult {
// TODO
// ----
// Restore purchases and return true if successful.
return RestorationResult.Success()
}
}
```
:::
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:
:::android
```kotlin Kotlin
// MainApplication.kt
class MainApplication : android.app.Application(), SuperwallDelegate {
override fun onCreate() {
super.onCreate()
Superwall.configure(this, "MY_API_KEY", MyPurchaseController(this))
// OR using the DSL
configureSuperwall("MY_API_KEY") {
purchaseController = MyPurchaseController(this@MainApplication)
}
}
}
```
:::
### 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:
:::android
```kotlin Kotlin
// When a subscription is purchased, restored, validated, expired, etc...
myService.subscriptionStatusDidChange {
if (it.hasActiveSubscription) {
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Active(entitlements))
} else {
Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive(entitlements))
}
}
```
:::
`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:
:::android
```kotlin Kotlin
Superwall.instance.subscriptionStatus.collect { status: SubscriptionStatus ->
// React to changes
}
```
:::
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
**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/android/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.
:::android
```kotlin Kotlin
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
when(eventInfo.event) {
is SuperwallEvent.TriggerFire -> {
MyAnalyticsService.shared.setUserAttributes(
mapOf(
"sw_experiment_${eventInfo.params.get("experiment_id").toString()}" to true,
"sw_variant_${eventInfo.params.get("variant_id").toString()}" to true
)
)
}
else -> {}
}
}
```
:::
:::flutter
```dart Flutter
@override
void handleSuperwallEvent(SuperwallEventInfo eventInfo) async {
final experimentId = eventInfo.params?['experiment_id'];
final variantId = eventInfo.params?['variant_id'];
switch (eventInfo.event.type) {
case EventType.triggerFire:
MyAnalyticsService.shared.setUserAttributes({
"sw_experiment_$experimentId": true,
"sw_variant_$variantId": true
});
break;
default:
break;
}
}
```
:::
:::expo
```typescript React Native
handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
const experimentId = eventInfo.params?['experiment_id']
const variantId = eventInfo.params?['variant_id']
if (!experimentId || !variantId) {
return
}
switch (eventInfo.event.type) {
case EventType.triggerFire:
MyAnalyticsService.shared.setUserAttributes({
`sw_experiment_${experimentId}`: true,
`sw_variant_${variantId}`: true
});
break;
default:
break;
}
}
```
:::
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/android/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":

You could create a custom placement [tap behavior](/paywall-editor-styling-elements#tap-behaviors) which fires when a segment is tapped:

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/android/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/android/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:
:::android
```kotlin
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
println("analytics event: ${eventInfo.event.rawName}")
MyAnalytics.shared.track(eventInfo.event.rawName, eventInfo.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`:
:::android
```kotlin
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
when(eventInfo.event) {
is SuperwallPlacement.AppClose -> TODO()
is SuperwallPlacement.AppInstall -> TODO()
is SuperwallPlacement.AppLaunch -> TODO()
is SuperwallPlacement.AppOpen -> TODO()
is SuperwallPlacement.DeepLink -> TODO()
is SuperwallPlacement.FirstSeen -> TODO()
is SuperwallPlacement.FreeTrialStart -> TODO()
is SuperwallPlacement.NonRecurringProductPurchase -> TODO()
is SuperwallPlacement.PaywallClose -> TODO()
is SuperwallPlacement.PaywallDecline -> TODO()
is SuperwallPlacement.PaywallOpen -> TODO()
is SuperwallPlacement.PaywallPresentationRequest -> TODO()
is SuperwallPlacement.PaywallProductsLoadComplete -> TODO()
is SuperwallPlacement.PaywallProductsLoadFail -> TODO()
is SuperwallPlacement.PaywallProductsLoadStart -> TODO()
is SuperwallPlacement.PaywallResponseLoadComplete -> TODO()
is SuperwallPlacement.PaywallResponseLoadFail -> TODO()
is SuperwallPlacement.PaywallResponseLoadNotFound -> TODO()
is SuperwallPlacement.PaywallResponseLoadStart -> TODO()
is SuperwallPlacement.PaywallWebviewLoadComplete -> TODO()
is SuperwallPlacement.PaywallWebviewLoadFail -> TODO()
is SuperwallPlacement.PaywallWebviewLoadStart -> TODO()
is SuperwallPlacement.PaywallWebviewLoadTimeout -> TODO()
is SuperwallPlacement.SessionStart -> TODO()
is SuperwallPlacement.SubscriptionStart -> TODO()
is SuperwallPlacement.SubscriptionStatusDidChange -> TODO()
is SuperwallPlacement.SurveyClose -> TODO()
is SuperwallPlacement.SurveyResponse -> TODO()
is SuperwallPlacement.TransactionAbandon -> TODO()
is SuperwallPlacement.TransactionComplete -> TODO()
is SuperwallPlacement.TransactionFail -> TODO()
is SuperwallPlacement.TransactionRestore -> TODO()
is SuperwallPlacement.TransactionStart -> TODO()
is SuperwallPlacement.TransactionTimeout -> TODO()
is SuperwallPlacement.TriggerFire -> TODO()
is SuperwallPlacement.UserAttributes -> TODO()
}
}
```
:::
Wanting to use events to see which product was purchased on a paywall? Check out this
[doc](/viewing-purchased-products).
---
# Advanced Configuration
Source: https://superwall.com/docs/android/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:
:::android
```kotlin
val options = SuperwallOptions()
options.logging.level = LogLevel.warn
options.logging.scopes = EnumSet.of(LogScope.paywallPresentation, LogScope.paywallTransactions)
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or you can set:
Superwall.instance.logLevel = LogLevel.warn
// Or use the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
logging {
level = LogLevel.warn
scopes = EnumSet.of(LogScope.paywallPresentation, LogScope.paywallTransactions)
}
}
}
```
:::
### 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`:
:::android
```kotlin
val options = SuperwallOptions()
options.paywalls.shouldPreload = false
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
paywalls {
shouldPreload = false
}
}
}
```
:::
Then, if you'd like to preload paywalls for specific placements you can use `preloadPaywalls(forPlacements:)`:
:::android
```kotlin
val eventNames = setOf("campaign_trigger")
Superwall.instance.preloadPaywalls(eventNames)
```
:::
If you'd like to preload all paywalls you can use `preloadAllPaywalls()`:
:::android
```kotlin
Superwall.instance.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`:
:::android
```kotlin
val options = SuperwallOptions()
options.isExternalDataCollectionEnabled = false
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
isExternalDataCollectionEnabled = false
}
}
```
:::
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`:
:::android
```kotlin
val options = SuperwallOptions()
options.paywalls.automaticallyDismiss = false
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
paywalls {
automaticallyDismiss = false
}
}
}
```
:::
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:
:::android
```kotlin
val options = SuperwallOptions()
options.paywalls.restoreFailed.title = "My Title"
options.paywalls.restoreFailed.message = "My message"
options.paywalls.restoreFailed.closeButtonTitle = "Close"
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
paywalls {
restoreFailed.title = "My Title"
restoreFailed.message = "My message"
restoreFailed.closeButtonTitle = "Close"
}
}
}
```
:::
### 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:
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`:
:::android
```kotlin
val options = SuperwallOptions()
options.paywalls.transactionBackgroundView = null
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
paywalls {
transactionBackgroundView = null
}
}
}
```
:::
### 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`:
:::android
```kotlin
val options = SuperwallOptions()
options.paywalls.shouldShowPurchaseFailureAlert = false
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
paywalls {
shouldShowPurchaseFailureAlert = false
}
}
}
```
:::
### 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:
:::android
```kotlin
val options = SuperwallOptions()
options.paywalls.shouldShowWebPurchaseConfirmationAlert = true
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
paywalls {
shouldShowWebPurchaseConfirmationAlert = true
}
}
}
```
:::
### 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:
:::android
```kotlin
val options = SuperwallOptions()
options.localeIdentifier = "en_GB"
Superwall.configure(
this,
apiKey = "MY_API_KEY",
options = options
)
// Or using the configuration DSL
configureSuperwall("MY_API_KEY") {
options {
localeIdentifier = "en_GB"
}
}
```
:::
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.
---
# Using the Superwall Delegate
Source: https://superwall.com/docs/android/guides/using-superwall-delegate
undefined
Use Superwall's delegate to extend our SDK's functionality across several surface areas by assigning to the `delegate` property:
:::android
```kotlin
class SWDelegate : SuperwallDelegate {
// Implement delegate methods here
}
// When configuring the SDK...
Superwall.instance.delegate = SWDelegate()
```
:::
Some common use cases for using the Superwall delegate include:
* **Custom actions:** [Respond to custom tap actions from a paywall.](/custom-paywall-events#custom-paywall-actions)
* **Respond to purchases:** [See which product was purchased from the presented paywall.](/viewing-purchased-products)
* **Analytics:** [Forward events from Superwall to your own analytics.](/3rd-party-analytics)
Below are some commonly used implementations when using the delegate.
### Superwall Events
Most of what occurs in Superwall can be viewed using the delegate method to respond to events:
:::android
```kotlin
class SWDelegate : SuperwallDelegate {
override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) {
// Handle any relevant events here...
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
}
}
}
}
```
:::
### Paywall Custom Actions
Using the [custom tap action](/custom-paywall-events), you can respond to any arbitrary event from a paywall:
:::android
```kotlin
class SWDelegate : SuperwallDelegate {
override fun handleCustomPaywallAction(withName: String) {
if (withName == "caffeineLogged") {
println("Custom paywall action: $withName")
}
}
}
```
:::
### Subscription status changes
You can be informed of subscription status changes using the delegate. If you need to set or handle the status on your own, use a [purchase controller](/advanced-configuration) — this function is only for informational, tracking or similar purposes:
:::android
```kotlin
class SWDelegate : SuperwallDelegate {
override fun subscriptionStatusDidChange(from: SubscriptionStatus, to: SubscriptionStatus) {
println("Subscription status changed from $from to $to")
}
}
```
:::
### Paywall events
The delegate also has callbacks for several paywall events, such dismissing, presenting, and more. Here's an example:
:::android
```kotlin
class SWDelegate : SuperwallDelegate {
override fun didPresentPaywall(withInfo: PaywallInfo) {
println("Paywall presented: $withInfo")
}
}
```
:::
---
# Vibe Coding
Source: https://superwall.com/docs/android/guides/vibe-coding
How to Vibe Code using the knowledge of the Superwall Docs
## Overview
We've built a few tools to help you Vibe Code using the knowledge of the Superwall Docs, right in your favorite AI tools:
* [Superwall Docs MCP](#superwall-docs-mcp) in Claude Code, Cursor, etc.
* [Superwall Docs GPT](#superwall-docs-gpt) in ChatGPT
And right here in the Superwall Docs:
* [Ask AI](#ask-ai)
* [Docs Links](#docs-links)
* [LLMs.txt](#llmstxt)
## Superwall Docs MCP
The Superwall Docs MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/tutorials/use-remote-mcp-server)) is a tool that allows your favorite AI tools to search the Superwall Docs and get context from the docs.
### Cursor
You can install the MCP server in Cursor by clicking this button:
[](https://cursor.com/en/install-mcp?name=superwall-docs-mcp\&config=eyJ1cmwiOiJodHRwczovL21jcC5zdXBlcndhbGwuY29tL21jcCJ9)
or by adding the following to your `~/.cursor/mcp.json` file:
```json
{
"mcpServers": {
"superwall-docs": {
"url": "https://mcp.superwall.com/mcp"
}
}
}
```
### Claude Code
You can install the MCP server in Claude Code by running the following command:
```bash
claude mcp add --transport sse superwall-docs https://mcp.superwall.com/sse
```
### Codex
You can install the MCP server in Codex by running the following command:
```bash
codex mcp add superwall --url https://mcp.superwall.com/mcp
```
## Superwall Docs GPT
You can use the [Superwall Docs GPT](https://chatgpt.com/g/g-6888175f1684819180302d66f4e61971-superwall-docs-gpt) right in the ChatGPT app, and use it to ask any Superwall question.
It has the full knowledge of the Superwall Docs, and can be used with all the ChatGPT features you love like using the context of your files straight from your IDE.
## Ask AI
The built-in [Ask AI tool](https://superwall.com/docs/ai) in the Superwall Docs is a great place to start if you have a question or issue.
## Docs Links
On each page of the Superwall Docs (including this one!), you can find in the top right corner:
* **Copy page**: to copy the page in Markdown format.
Also in the dropdown menu, you can access these options:
* **View as markdown**: to view the page in Markdown format
* **Open in ChatGPT**, **Open in Claude**: to open the page in the respective AI tool and add the page as context for your conversation
## LLMs.txt
The Superwall Docs website has `llms.txt` and `llms-full.txt` files, in total and for each SDK, that you can use to add context to your LLMs.
`llms.txt` is a summary of the docs with links to each page.
`llms-full.txt` is the full text of all of the docs.
| SDK | Summary | Full Text |
| ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) |
| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) |
| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) |
| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) |
| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) |
| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) |
| React Native (Deprecated) | [`llms-react-native.txt`](https://superwall.com/docs/llms-react-native.txt) | [`llms-full-react-native.txt`](https://superwall.com/docs/llms-full-react-native.txt) |
| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) |
| Web Checkout | [`llms-web-checkout.txt`](https://superwall.com/docs/llms-web-checkout.txt) | [`llms-full-web-checkout.txt`](https://superwall.com/docs/llms-full-web-checkout.txt) |
To minimize token use, we recommend using the files specific to your SDK.
---
# Using RevenueCat
Source: https://superwall.com/docs/android/guides/using-revenuecat
undefined
Not using RevenueCat? No problem! Superwall works out of the box without any additional SDKs.
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.
You can integrate RevenueCat with Superwall using several approaches:
1. [**Using a purchase controller:**](#using-a-purchase-controller) Use this route if you want to maintain control over purchasing logic and code.
2. [**Using PurchasesAreCompletedBy:**](#using-purchasesarecompletedby) Here, you don't use a purchase controller and you tell RevenueCat that purchases are completed by your app using StoreKit. In this mode, RevenueCat will observe the purchases that the Superwall SDK makes. For more info [see here](https://www.revenuecat.com/docs/migrating-to-revenuecat/sdk-or-not/finishing-transactions).
## Using a purchase controller
### 1. Create a `PurchaseController`
Create a new file called `RCPurchaseController`, then copy and paste the following:
:::android
```kotlin
package com.superwall.superapp
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.ProductDetails
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.LogLevel
import com.revenuecat.purchases.ProductType
import com.revenuecat.purchases.PurchaseParams
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.getCustomerInfoWith
import com.revenuecat.purchases.interfaces.GetStoreProductsCallback
import com.revenuecat.purchases.interfaces.PurchaseCallback
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
import com.revenuecat.purchases.models.StoreProduct
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.models.SubscriptionOption
import com.revenuecat.purchases.models.googleProduct
import com.revenuecat.purchases.purchaseWith
import com.superwall.sdk.Superwall
import com.superwall.sdk.delegate.PurchaseResult
import com.superwall.sdk.delegate.RestorationResult
import com.superwall.sdk.delegate.subscription_controller.PurchaseController
import com.superwall.sdk.models.entitlements.Entitlement
import com.superwall.sdk.models.entitlements.SubscriptionStatus
import kotlinx.coroutines.CompletableDeferred
suspend fun Purchases.awaitProducts(productIds: List): List {
val deferred = CompletableDeferred>()
getProducts(
productIds,
object : GetStoreProductsCallback {
override fun onReceived(storeProducts: List) {
deferred.complete(storeProducts)
}
override fun onError(error: PurchasesError) {
deferred.completeExceptionally(Exception(error.message))
}
},
)
return deferred.await()
}
interface PurchaseCompletion {
var storeTransaction: StoreTransaction
var customerInfo: CustomerInfo
}
// Create a custom exception class that wraps PurchasesError
private class PurchasesException(
val purchasesError: PurchasesError,
) : Exception(purchasesError.toString())
suspend fun Purchases.awaitPurchase(
activity: Activity,
storeProduct: StoreProduct,
): PurchaseCompletion {
val deferred = CompletableDeferred()
purchase(
PurchaseParams.Builder(activity, storeProduct).build(),
object : PurchaseCallback {
override fun onCompleted(
storeTransaction: StoreTransaction,
customerInfo: CustomerInfo,
) {
deferred.complete(
object : PurchaseCompletion {
override var storeTransaction: StoreTransaction = storeTransaction
override var customerInfo: CustomerInfo = customerInfo
},
)
}
override fun onError(
error: PurchasesError,
p1: Boolean,
) {
deferred.completeExceptionally(PurchasesException(error))
}
},
)
return deferred.await()
}
suspend fun Purchases.awaitRestoration(): CustomerInfo {
val deferred = CompletableDeferred()
restorePurchases(
object : ReceiveCustomerInfoCallback {
override fun onReceived(purchaserInfo: CustomerInfo) {
deferred.complete(purchaserInfo)
}
override fun onError(error: PurchasesError) {
deferred.completeExceptionally(error as Throwable)
}
},
)
return deferred.await()
}
class RevenueCatPurchaseController(
val context: Context,
) : PurchaseController,
UpdatedCustomerInfoListener {
init {
Purchases.logLevel = LogLevel.DEBUG
Purchases.configure(
PurchasesConfiguration
.Builder(
context,
"android_rc_key",
).build(),
)
// Make sure we get the updates
Purchases.sharedInstance.updatedCustomerInfoListener = this
}
fun syncSubscriptionStatus() {
// Refetch the customer info on load
Purchases.sharedInstance.getCustomerInfoWith {
if (hasAnyActiveEntitlements(it)) {
setSubscriptionStatus(
SubscriptionStatus.Active(
it.entitlements.active
.map {
Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
}.toSet(),
),
)
} else {
setSubscriptionStatus(SubscriptionStatus.Inactive)
}
}
}
/**
* Callback for rc customer updated info
*/
override fun onReceived(customerInfo: CustomerInfo) {
if (hasAnyActiveEntitlements(customerInfo)) {
setSubscriptionStatus(
SubscriptionStatus.Active(
customerInfo.entitlements.active
.map {
Entitlement(it.key, Entitlement.Type.SERVICE_LEVEL)
}.toSet(),
),
)
} else {
setSubscriptionStatus(SubscriptionStatus.Inactive)
}
}
/**
* Initiate a purchase
*/
override suspend fun purchase(
activity: Activity,
productDetails: ProductDetails,
basePlanId: String?,
offerId: String?,
): PurchaseResult {
// Find products matching productId from RevenueCat
val products = Purchases.sharedInstance.awaitProducts(listOf(productDetails.productId))
// Choose the product which matches the given base plan.
// If no base plan set, select first product or fail.
val product =
products.firstOrNull { it.googleProduct?.basePlanId == basePlanId }
?: products.firstOrNull()
?: return PurchaseResult.Failed("Product not found")
return when (product.type) {
ProductType.SUBS, ProductType.UNKNOWN ->
handleSubscription(
activity,
product,
basePlanId,
offerId,
)
ProductType.INAPP -> handleInAppPurchase(activity, product)
}
}
private fun buildSubscriptionOptionId(
basePlanId: String?,
offerId: String?,
): String =
buildString {
basePlanId?.let { append("$it") }
offerId?.let { append(":$it") }
}
private suspend fun handleSubscription(
activity: Activity,
storeProduct: StoreProduct,
basePlanId: String?,
offerId: String?,
): PurchaseResult {
storeProduct.subscriptionOptions?.let { subscriptionOptions ->
// If subscription option exists, concatenate base + offer ID.
val subscriptionOptionId = buildSubscriptionOptionId(basePlanId, offerId)
// Find first subscription option that matches the subscription option ID or default
// to letting revenuecat choose.
val subscriptionOption =
subscriptionOptions.firstOrNull { it.id == subscriptionOptionId }
?: subscriptionOptions.defaultOffer
// Purchase subscription option, otherwise fail.
if (subscriptionOption != null) {
return purchaseSubscription(activity, subscriptionOption)
}
}
return PurchaseResult.Failed("Valid subscription option not found for product.")
}
private suspend fun purchaseSubscription(
activity: Activity,
subscriptionOption: SubscriptionOption,
): PurchaseResult {
val deferred = CompletableDeferred()
Purchases.sharedInstance.purchaseWith(
PurchaseParams.Builder(activity, subscriptionOption).build(),
onError = { error, userCancelled ->
deferred.complete(
if (userCancelled) {
PurchaseResult.Cancelled()
} else {
PurchaseResult.Failed(
error.message,
)
},
)
},
onSuccess = { _, _ ->
deferred.complete(PurchaseResult.Purchased())
},
)
return deferred.await()
}
private suspend fun handleInAppPurchase(
activity: Activity,
storeProduct: StoreProduct,
): PurchaseResult =
try {
Purchases.sharedInstance.awaitPurchase(activity, storeProduct)
PurchaseResult.Purchased()
} catch (e: PurchasesException) {
when (e.purchasesError.code) {
PurchasesErrorCode.PurchaseCancelledError -> PurchaseResult.Cancelled()
else ->
PurchaseResult.Failed(
e.message ?: "Purchase failed due to an unknown error",
)
}
}
/**
* Restore purchases
*/
override suspend fun restorePurchases(): RestorationResult {
try {
if (hasAnyActiveEntitlements(Purchases.sharedInstance.awaitRestoration())) {
return RestorationResult.Restored()
} else {
return RestorationResult.Failed(Exception("No active entitlements"))
}
} catch (e: Throwable) {
return RestorationResult.Failed(e)
}
}
/**
* Check if the customer has any active entitlements
*/
private fun hasAnyActiveEntitlements(customerInfo: CustomerInfo): Boolean {
val entitlements =
customerInfo.entitlements.active.values
.map { it.identifier }
return entitlements.isNotEmpty()
}
private fun setSubscriptionStatus(subscriptionStatus: SubscriptionStatus) {
if (Superwall.initialized) {
Superwall.instance.setSubscriptionStatus(subscriptionStatus)
}
}
}
```
:::
As discussed in [Purchases and Subscription Status](/docs/advanced-configuration), this `CustomPurchaseControllerProvider` is responsible for handling the subscription-related logic using the modern hooks-based approach.
### 2. Configure Superwall (Continued)
The example above shows the complete setup. The `CustomPurchaseControllerProvider` wraps your `SuperwallProvider` and handles all purchase and restore logic through RevenueCat.
For more advanced implementations, see the [example app](https://github.com/superwall/expo-superwall/tree/main/example).
**Legacy Approach**: If you're migrating from the old SDK or need the class-based purchase controller, you can use `expo-superwall/compat`. However, we recommend using the modern `CustomPurchaseControllerProvider` approach shown above.
### Removed Legacy Code Section
The following section contains the legacy class-based approach. Skip to the next section for the modern configuration.
As discussed in [Purchases and Subscription Status](/docs/advanced-configuration), this `PurchaseController` is responsible for handling the subscription-related logic. Take a few moments to look through the code to understand how it does this.
### 2. Configure Superwall
Initialize an instance of `RCPurchaseController` and pass it in to `Superwall.configure(apiKey:purchaseController)`:
:::android
```kotlin
val purchaseController = RCPurchaseController(this)
Superwall.configure(
this,
"MY_API_KEY",
purchaseController
)
// Make sure we sync the subscription status
// on first load and whenever it changes
purchaseController.syncSubscriptionStatus()
```
:::
### 3. Sync the subscription status
Then, call `purchaseController.syncSubscriptionStatus()` to keep Superwall's subscription status up to date with RevenueCat.
That's it! Check out our sample app for working examples:
:::android
* [Android](https://github.com/superwall/Superwall-Android/tree/develop/example/app/src/revenuecat)
:::
## Using PurchasesAreCompletedBy
If you're using RevenueCat's [PurchasesAreCompletedBy](https://www.revenuecat.com/docs/migrating-to-revenuecat/sdk-or-not/finishing-transactions), you don't need to create a purchase controller. Register your placements, present a paywall — and Superwall will take care of completing any purchase the user starts. However, there are a few things to note if you use this setup:
1. Here, you aren't using RevenueCat's [entitlements](https://www.revenuecat.com/docs/getting-started/entitlements#entitlements) as a source of truth. If your app is multiplatform, you'll need to consider how to link up pro features or purchased products for users.
2. If you require custom logic when purchases occur, then you'll want to add a purchase controller. In that case, Superwall handles purchasing flows and RevenueCat will still observe transactions to power their analytics and charts.
3. Be sure that user identifiers are set the same way across Superwall and RevenueCat.
For more information on observer mode, visit [RevenueCat's docs](https://www.revenuecat.com/docs/migrating-to-revenuecat/sdk-or-not/finishing-transactions).
---
# Changelog
Source: https://superwall.com/docs/android/changelog
Release notes for the Superwall Android SDK
## 2.6.6
## Enhancements
* Add dynamic notification support and scheduling enabling deeper personalization of notifications
* Provides a `CustomerInfo` class API, allowing you to observe the customer's purchases and subscription lifecycle via `Superwall.instance.customerInfo` flow
* Provides a new delegate method to observe customer info changes - `fun customerInfoDidChange(from: CustomerInfo, to: CustomerInfo)`
* Provides a `CustomerInfoDidChange` event to track customer info changes
* Overrides `Collection.plus`and `.toSet` methods to ensure our merging methods are used.
* Provides a `userAttributesDidChange(newAttributes: Map)` method in Superwall Delegate to track external (i.e. paywall) attribute changesg
* Allows triggering a `transaction_abandon` offer on a `paywall_decline` offer and vice-versa, whereas previously it would trigger a presentation error.
* Add a `Superwall.teardown` method and `Superwall.instance.refreshConfiguration()` for development with hot-reload based frameworks
## ⚠️ Warning ⚠️
If you are using a Purchase Controller and web2app or app2web purchases, you will have to update your purchase controller
to listen to `Superwall.instance.customerInfo` which will provide you with the relevant web entitlements and call
`setSubscriptionStatus` accordingly.
## 2.6.5
## Dependencies
* Reverts `androidx.lifecycle:lifecycle-runtime-ktx` to 2.8.4 to ensure old Compose BOM compatiblity
## Enhancements
* Improves error messaging in play store errors
## Fixes
* Fixes edge case bug with wrong entitlement being matched in cases where product ID's match and base plans differentiate by suffix only
* Fixes issue with composable paywall state updates not firing in onAttach
## 2.6.4
## Enhancements
* Improves error and timeout handling
* Hardens paywall recreation in case of render process crash
## 2.6.3
## Enhancements
* Adds `productIdentifier` to RedemptionResult's `PaywallInfo` object
## Fixes
* Fixes nested scrolling issue in Modal webviews
* Removes node removal for `com.google.android.gms.permission.AD_ID` from Manifest
* Ensures remote entitlements in the background refresh without feature flags
## 2.6.2
## Enhancements
* Adds `Superwall.instance.consume(purchaseToken)` method to help easily consume in-app purchases
## Fixes
* Fixes issue with deeplink params not being handled properly in some cases
* Fixes issue with Drawer and Modal displays on Android 14 Samsung devices
* Fixes selection issue with some OTP, ensures after consuming the Status is synced
## 2.6.1
## Enhancements
* Enables Stripe and Paddle checkout via in-app payment sheets
* Improves product handling and redemption for Stripe and Paddle
## Fixes
* Fixes issue with Google's Play Billing library auto-reconnection
## 2.6.0 ⚠️ \[Deprecated]
## Notes
* This version is deprecated due to discovery of an issue in Play Billing library which could cause runtime issues
* Please use version 2.6.1
## 2.5.8
## Fixes
* Fix lifetime purchase entitlements not being discovered in some cases on purchase
* Fix potential ANR issues where some animations would end up looping over on main thread
* Fix webview client not behaving properly when using a resetted paywall
## 2.6.0-alpha
* Add app2web support, allowing users to purchase Stripe or Paddle products without leaving your app
* Add `PaymentSheet` purchase type enabling quick bottom sheet purchases
* Add support for Android app links purchase redeeming
## 2.5.7
## Fixes
* Fix `demandScore` and `demandTier` getting removed from some events
* Fixes paywall navigation resetting after backgrounding
* Removes webview flags which can cause off-screen render issues
## 2.5.6
## Enhancements
* Add support for rerouting back button if enabled in paywall settings
* Handled by `SuperwallOptions.PaywallOptions.onBackPressed`, which enables you to consume the back press or let the SDK consume it
* Add support for redeeming web entitlements with Paddle
## Fixes
* Fix binary .so file regression to ensure 16kb page size compatibility
* Fix potential issue with paywall not dismissing due to paywall\_decline concurrency issue
## 2.5.5
## Enhancements
* Expose `signature` on `StoreTransaction`
## Fixes
* ⚠️ Important - in the recent versions we have added usage of Google AppSetId and AdID to enable automatic attribution with Google's ad networks.
* Due to issues with Google's detection of the usage, they have been removed temporarily and will be added back once the issue is resolved.
* As an alternative, you can set those attribution identifiers using `AttributionProvider.GOOGLE_APP_SET | AttributionProvider.GOOGLE_ADS`
## 2.5.4
## Enhancements
* `PaywallComposable` now reapplies theme on system change and `PaywallView` now exposes an `onThemeChanged()` method
* `DeepLink` event now exposes query params
## 2.5.3
## Enhancements
* Adds ability to specify a custom height and corner radius for the drawer presentation style.
* Adds ability to display a `Popup` presentation style
* Adds ability to request reviews from paywall actions, including:
* Value `device.reviewRequestCount` that returns total request counts
* Method `device.reviewRequestsInHour|Day|Week|Year|reviewRequestsSinceInstall` computed methods for granular targeting
* Adds `Superwall.instance.setIntegrationAttributes` method enabling you to set integration identifiers for the users from different platforms (Adjust, Mixpanel, Meta, etc.)
* Adds `Superwall.instance.showAlert` method enabling you to easily show an alert over the current paywall
* Adds `PaywallOptions.timeoutAfter` to easily control timeout of paywalls when not using fallback loading
* Adds `Superwall.instance.setIntegrationAttributes` and `Superwall.instance.integrationAttributes` to set integration identifiers (i.e. Mixpanel, Appsflyer, etc)
* Adds user attributes to `TransactionComplete` and `PaywallOpen` events
* Adds device ID to device attributes
## Fixes
* Fixes memory leak issues in `PaywallBuilder` would keep the paywall view alive in some cases
* Fixes issue where some events would be missing properties
## 2.5.1
## Enhancements
* Improves subscribed user experience in cases with slow configuration
## 2.5.0
## Enhancements
* Updates Google Play billing library to v8. Unfortunately, Google has broken backwards compatibility with previous versions, so if you're using the standalone library too ensure it is compatible with v8.
* Adds kotlin version to device variables
## Fixes
* Reduces noisy logging when product is missing an offer
* Ensures that getting experimental properties works for consumable products
## 2.4.1
## Fixes
* Google Play Billing integration with newer libraries (such as RC 9.\*)
* Reduces noisy logging when product is missing an offer
* Ensures that getting experimental properties works for consumable products
## 2.4.0
## Enhancements
* Increases superscript version to 1.0.2 with improved type and null safety
## 2.3.3
## Fixes
* Ensure properties are always properly serialized
## 2.3.2
## Enhancements
* Adds new properties to count placement occurrences in specific time: `placementsInHours`, `placementsInDay`, `placementsInWeek`, `placementsInMonth`, `placementsSinceInstall`
## Fixes
* Fixes an issue where a redemption could succeed but throw an error
* Fixes an issue with receipt manager when there is no purchases and experimental properties are enabled
## 2.3.1
## Fixes
* Fixes an issue where entitlements would not be reset on time
* Ensures `redeem` is only done on initial config not on config refresh
## Enhancements
* Adds `overrideProductsByName` property to allow globally overriding products on paywalls. This property accepts a map of product names to product identifiers (strings). Local overrides provided via `PaywallOverrides` take precedence over global overrides.
* Adds `ProductOverride` sealed class to provide flexible product override handling with support for both product IDs and `StoreProduct` objects.
## 2.3.0
## Enhancements
* Deprecated `Superwall.instance.handleDeepLink` in favor of static `Superwall.handleDeepLink` to ensure links received before `configure` completion are handled properly
* Adds `externalAcountId`, provided to Google Play billing upon purchase as a SHA256 of the userId or the userId itself if `passIdentifiersToPlayStore` option is provided.
* Adds a `SuperwallOption` named `enableExperimentalDeviceVariables`. When set to true, this enables additional device-level variables: `latestSubscriptionPeriodType`, `latestSubscriptionState`, and `latestSubscriptionWillAutoRenew`. These properties provide information about the most recent Google Play subscription on the device and can be used in audience filters. Note that due to their experimental nature, they are subject to change in future updates.
* Update `com.android.billingclient` to version 7.1.1 to align with Google's latest requirements
## Fixes
* Fixes issues with paywall destruction when activity performs a hot reload (i.e. during update)
* Fixes issue where the feature block would be triggered on non-gated paywalls when the app is minimised
## 2.2.3
## Fixes
* Fix potential issue with device enrichment and attributes synchronisation causing a lock
## 2.2.2
## Enhancements
* Adds `demandScore` and `demandTier` to device attributes using an off-device advanced machine learning model. A user is assigned these based on a variety of factors to determine whether they're more or less likely to convert and can be used within audience filters.
* Adds `deviceTier` to the device attributes using an on-device scoring system to place the device in a tier based on it's capabilities. This can be used in audience filters to display different paywalls based on the user's device capabilities. The value can be one of the following values `ultraLow`, `low`, `mid`, `high`, `ultra_high`, `unknown`. NOTE: This property is still experimental
## Fix
* Fixes potential issue in `web2app` with subscription being overriden by the polling due to extra API calls
## 2.2.0
## Enhancements
* Updates binaries to work on 16kb page sizes
## 2.1.2
## Fixes
* Fix issue with deep link referrer throwing a DeadObjectException
## 2.1.1
## Enhancements
* Add optimisticLoading paywall option that hides the shimmer when HTML is loaded
* Prevent stopping the paywall handler listening with onDismiss when reason is None
* Improve PaywallBuilder API for non-kotlin and non-coroutine users
* Expose `deviceAttributes()` function to retrieve session device attributes
## 2.1.0
## Enhancements
* Updates kotlin version to 2.0.21
* Updates `compileSDK` to 35
* Adds web checkout and redemption support
* Adds SuperwallDelegate methods `willRedeemLink` and `didRedeemLink`
## Fixes
* Removes lock while reading cache that could cause ANR on the main thread
* Fixes issue where experiment and variant ID would be missing due to concurrency issues
## 2.1.0-beta.1
## Enhancements
* Updates kotlin version to 2.0.21
* Updates `compileSDK` to 35
* Adds web checkout and redemption support
* Adds SuperwallDelegate methods `willRedeemLink` and `didRedeemLink`
## 2.0.8
## Fixes
* Fixes the serialization issue with Kotlin 2.0
## 2.0.7
## Enhancements
* Improves how errors are handled when loading, improving the UX and reloading in real failure cases
* Added `device.subscriptionStatus` to the device object
## Fixes
* Fixes an issue where users of kotlin 2.0 would experience a `NoAudienceMatch` when evaluating rules
## 2.0.6
## Fixes
* Fix potential crash while setting render priority
## 2.0.5
## Fixes
* Fix issue with `original_transaction_id` missing when using a `PurchaseController`
# 2.0.4
## Enhancements
* Provide overloads for Java interop
* Provide utility functions for Java interop with `PurchaseController`
# 2.0.3
## Enhancements
* Renames `SuperwallPlacement` back to `SuperwallEvent`
# 2.0.2
## Fixes
* Fixes issue with `NoAudienceMatch` appearing on some devices and issues with certain campaign rules
# 2.0.1
## Enhancements
* Changes back to `handleSuperwallEvent` naming with a deprecation notice and a typealias for previous methods
## Fixes
* Removes extra failure logging when displaying alerts
* Finds nearest activity instead of relying just on Context in `PaywallComposable`
* Improves cleanup in `PaywallComposable`
# 2.0.0
Our 2.0.0 release brings some major and minor changes to both our API's and core features. For more information, please look at our [migration docs](https://superwall.com/docs/migrating-to-v2-android)
## Enhancements
* Adds `PaywallBuilder` class as an alternative to existing `getPaywallView` method. This provides a cleaner API and an ability to change purchase loading bar and shimmer view.
* Ensure safety of static webview calls that are known to fail randomly due to Webview's internal issues
* Adds `purchase` method to `Superwall` you can use to purchase products without having to resort on paywalls. To purchase a product you can pass it in one of the following objects:
* Google Play's `ProductDetails`
* Superwall's `StoreProduct` object
* Or a string containing the product identifier, i.e. `Superwall.instance.purchase("product_id:base_plan:offer")`
* Adds `restorePurchases` method to `Superwall` you can use to handle restoring purchases
* Adds `getProducts` method to `Superwall` you can use to retrieve a list of `ProductDetails` given the product ID, i.e. i.e. `Superwall.instance.purchase("product_id:base_plan:offer")`
* Adds support for observing purchases done outside of Superwall paywalls. You can now observe purchases done outside of Superwall paywalls by setting the `shouldObservePurchases` option to true and either:
* Manually by calling `Superwall.instance.observe(PurchasingObserverState)` or utility methods `Superwall.instance.observePurchaseStart/observePurchaseError/observePurchaseResult`
* Automatically by replacing `launchBillingFlow` with `launchBillingFlowWithSuperwall`. This will automatically observe purchases done outside of Superwall paywalls.
* Adds consumer proguard rules to enable consumer minification
* `Superwall.instance` now provides blocking or callback based version of multiple calls, suffixed with `*Sync`
* Improves preloading performance and reduces impact on the main thread
* Reduces minSDK to 22
## Breaking Changes
* `SuperwallPaywallActivity` and `PaywallView` have been moved into `com.superwall.sdk.paywall.view` package from `com.superwall.sdk.paywall.vc` package.
* Removes `PaywallComposable` and Jetpack Compose support from the main SDK artifact in favor of `Superwall-Compose` module for Jetpack Compose support:
* You can find it at `com.superwall.sdk:superwall-compose:2.0.0-alpha`
* Usage remains the same as before, but now you need to include the `superwall-compose` module in your project.
* Removed methods previously marked as Deprecated
* `SubscriptionStatus.Active` now takes in a set of `Entitlements`, while `Inactive` and `Active` have been turned into objects.
* `Superwall.instance.register` now uses `placement` instead of `event` as the argument name
* `preloadPaywalls` now uses `placementNames` instead of `eventNames` as the argument name
* `PaywallPresentationHandler.onDismiss` now has two arguments, `PaywallInfo` and `PaywallResult`
* `PaywallComposable` now uses `placement` argument instead of `event`
* `TriggerResult.NoRuleMatch` and `TriggerResult.EventNotFound` have been renamed to `TriggerResult.NoAudienceMatch` and `TriggerResult.PlacementNotFound`
* `PresentationResult.NoRuleMatch` and `PresentationResult.EventNotFound` have been renamed to `PresentationResult.NoAudienceMatch` and `PresentationResult.PlacementNotFound`
* `SuperwallEvent` has been renamed to `SuperwallPlacement`, belonging properties with `eventName` have been renamed to `placementName`
* `SuperwallEventInfo` has been renamed to `SuperwallPlacementInfo`
* `ComputedPropertyRequest.eventName` has been renamed to `ComputedPropertyRequest.placementName`
* `Superwall.instance.events` has been renamed to `Superwall.instance.placements`
* `LogScope.events` has been renamed to `LogScope.placements`
* `PaywallPresentationRequestStatusReason.EventNotFound` has been renamed to `PaywallPresentationRequestStatusReason.PlacementNotFound`
* `PaywallSkippedReason.EventNotFound` has been renamed to `PaywallSkippedReason.PlacementNotFound`
* `SuperwallDelegate.handleSuperwallEvent` method has been renamed to `SuperwallDelegate.handleSuperwallPlacement`
* Removed `PurchaseResult.Restored`
# 2.0.0-beta.5
## Breaking changes
* `Superwall.instance.register` now uses `placement` instead of `event` as the argument name
* `preloadPaywalls` now uses `placementNames` instead of `eventNames` as the argument name
* Superwall's `PaywallPresentationHandler.onDismiss` now has two arguments, `PaywallInfo` and `PaywallResult`
* `PaywallComposable` now uses `placement` argument instead of `event`
* Remove `PurchaseResult.Restored`
# 2.0.0-beta.4
## Breaking changes
* `SuperwallEvents.entitlementStatusDidChange` has been renamed to `SuperwallEvents.subscriptionStatusDidChange`
* `SuperwallDelegate.entitlementStatusDidChange` has been renamed to `SuperwallEvents.entitlementStatusDidChange`
* `TriggerResult.NoRuleMatch` and `TriggerResult.EventNotFound` have been renamed to `TriggerResult.NoAudienceMatch` and `TriggerResult.PlacementNotFound`
* `PresentationResult.NoRuleMatch` and `PresentationResult.EventNotFound` have been renamed to `PresentationResult.NoAudienceMatch` and `PresentationResult.PlacementNotFound`
## 2.0.0-beta.3
### Breaking changes
* `SuperwallEvent` has been renamed to `SuperwallPlacement`, belonging properties with `eventName` have been renamed to `placementName`
* `SuperwallEventInfo` has been renamed to `SuperwallPlacementInfo`
* `ComputedPropertyRequest.eventName` has been renamed to `ComputedPropertyRequest.placementName`
* `Superwall.instance.events` has been renamed to `Superwall.instance.placements`
* `LogScope.events` has been renamed to `LogScope.placements`
* `PaywallPresentationRequestStatusReason.EventNotFound` has been renamed to `PaywallPresentationRequestStatusReason.PlacementNotFound`
* `PaywallSkippedReason.EventNotFound` has been renamed to `PaywallSkippedReason.PlacementNotFound`
* `SuperwallDelegate.handleSuperwallEvent` method has been renamed to `SuperwallDelegate.handleSuperwallPlacement`
## 2.0.0-beta.2
### Breaking Changes
* API Changes:
* Migration of `setEntitlementStatus` to `setSubscriptionStatus`
* Exposing `Superwall.instance.entitlementsStatus`
* Migration of `SuperwallDelegate.entitlementStatusDidChange` to `SuperwallDelegate.subscriptionStatusDidChange`
## 2.0.0-beta.1
## Enhancements
* Add `PaywallBuilder` class as an alternative to existing `getPaywallView` method. This provides a cleaner API and an ability to change purchase loading bar and shimmer view.
* Add callback versions of new 2.0 methods
* Ensure safety of static webview calls that are known to fail randomly due to Webview's internal issues
## 2.0.0-Alpha.1
### Breaking Changes
* `SuperwallPaywallActivity` and `PaywallView` have been moved into `com.superwall.sdk.paywall.view` package from `com.superwall.sdk.paywall.vc` package.
* Removes `PaywallComposable` and Jetpack Compose support from the main SDK artifact in favor of `Superwall-Compose` module for Jetpack Compose support:
* You can find it at `com.superwall.sdk:superwall-compose:2.0.0-alpha`
* Usage remains the same as before, but now you need to include the `superwall-compose` module in your project.
* Removed methods previously marked as Deprecated
* Removes `SubscriptionStatus`, together with belonging update methods and `subscriptionStatusDidChange` callback.
* These are replaced with `EntitlementStatus` and `entitlementStatusDidChange` callback. You can find more details on this migration in our docs.
### Enhancements
* Adds `purchase` method to `Superwall` you can use to purchase products without having to resort on paywalls. To purchase a product you can pass it in one of the following objects:
* Google Play's `ProductDetails`
* Superwall's `StoreProduct` object
* Or a string containing the product identifier, i.e. `Superwall.instance.purchase("product_id:base_plan:offer")`
* Adds `restorePurchases` method to `Superwall` you can use to handle restoring purchases
* Adds `getProducts` method to `Superwall` you can use to retrieve a list of `ProductDetails` given the product ID, i.e. i.e. `Superwall.instance.purchase("product_id:base_plan:offer")`
* Adds support for observing purchases done outside of Superwall paywalls. You can now observe purchases done outside of Superwall paywalls by setting the `shouldObservePurchases` option to true and either:
* Manually by calling `Superwall.instance.observe(PurchasingObserverState)` or utility methods `Superwall.instance.observePurchaseStart/observePurchaseError/observePurchaseResult`
* Automatically by replacing `launchBillingFlow` with `launchBillingFlowWithSuperwall`. This will automatically observe purchases done outside of Superwall paywalls.
* Adds consumer proguard rules to enable consumer minification
* Reduces minSDK to 22
* Adds `purchaseToken` to purchase events
* `Superwall.instance` now provides blocking or callback based version of multiple calls, suffixed with `*Sync`
* Improves preloading performance and reduces impact on the main thread
## 1.5.5
## Fixes
* Fixes potential distribution issues for variant selection in edge cases
* Fixes potential memory leaks of paywall calling activity
## 1.5.4
## Fixes
* Fixes issue when a paywall would dismiss with `ForNextPaywall` but next paywall could not be shown due to triggers or limits. Now it resolves into the proper dismiss status.
## 1.5.3
### Enhancements
* Add `purchaseToken` to TransactionComplete
## 1.5.2
### Fixes
* Fix chromium crashes caused by race conditions in webview's implementation
## 1.5.1
### Enhancements
* Updates superscript dependencies to reduce minSDK version
### Fixes
* Adds consumer proguard rules to avoid minifying JNA classes during minification
## 1.5.0
### Enhancements
* Adds `shimmerView_start` and `shimmerView_complete` events to track the loading of the shimmer animation.
* Makes `hasFreeTrial` match iOS SDK behavior by returning `true` for both free trials and non-free introductory offers
* Adds `Superwall.instance.events` - A SharedFlow instance emitting all Superwall events as `SuperwallEventInfo`. This can be used as an alternative to a delegate for listening to events.
* Adds a new shimmer animation
* Adds support for SuperScript expression evaluator
### Fixes
* Fixes concurrency issues with subscriptions triggered in Cordova apps
## 1.5.0-beta.2
## Enhancements
* Adds `shimmerView_start` and `shimmerView_complete` events to track the loading of the shimmer animation.
* Makes `hasFreeTrial` match iOS SDK behavior by returning `true` for both free trials and non-free introductory offers
## 1.5.0-beta.1
## Enhancements
* Adds `Superwall.instance.events` - A SharedFlow instance emitting all Superwall events as `SuperwallEventInfo`. This can be used as an alternative to a delegate for listening to events.
* Adds a new shimmer animation
* Adds support for SuperScript expression evaluator
## 1.4.1
### Enhancements
* Adds `appVersionPadded` attribute
### Fixes
* Fixes issue where `PaywallPresentationHandler.onError` would be skipped in case of `BillingError`s
## 1.4.0
## Enhancements
* Improves paywall loading and preloading performance
* Reduces impact of preloading on render performance
* Updates methods to return `kotlin.Result` instead of relying on throwing exceptions
* This introduces some minor breaking changes:
* `configure` completion block now provides a `Result` that can be used to check for success or failure
* `handleDeepLink` now returns a `Result`
* `getAssignments` now returns a `Result>`
* `confirmAllAssignments` now returns a `Result>`
* `getPresentationResult` now returns a `Result`
* `getPaywallComponents` now returns a `Result`
* Removes Gson dependency
* Adds `isScrollEnabled` flag to enable remote control of Paywall scrollability
* Adds `PaywallResourceLoadFail` event to enable tracking of failed resources in Paywall
* Improves bottom navigation bar color handling
## Fixes
* Fixes issue where paywalls without fallback would fail to load and missing resource would cause a failure event
* Fixes issue with `trialPeriodDays` rounding to the higher value instead of lower, i.e. where `P4W2D` would return 28 days instead of 30, it now returns 30.
* Fixes issue with system navigation bar not respecting paywall color
* Fixes issues with cursor allocation in Room transaction
* Improves handling of chromium render process crash
## 1.4.0-beta.3
* Fixes issue where paywalls without fallback would fail to load and missing resource would cause a failure event
## 1.4.0-beta.2
## Enhancements
* Removes Gson dependency
* Adds `isScrollEnabled` flag to enable remote controll of Paywall scrollability
## Fixes
* Fixes issue with `trialPeriodDays` rounding to the higher value instead of lower, i.e. where `P4W2D` would return 28 days instead of 30, it now returns 30.
* Fixes issue with system navigation bar not respecting paywall color
* Reduces impact of preloading on render performance
* Fixes issues with cursor allocation in Room transaction
## 1.4.0-beta.1
* Updates methods to return `kotlin.Result` instead of relying on throwing exceptions
* This introduces some minor breaking changes:
* `configure` completion block now provides a `Result` that can be used to check for success or failure
* `handleDeepLink` now returns a `Result`
* `getAssignments` now returns a `Result>`
* `confirmAllAssignments` now returns a `Result>`
* `getPresentationResult` now returns a `Result`
* `getPaywallComponents` now returns a `Result`
## 1.3.1
### Fixes
* Fixes issue when destroying activities during sleep would cause a background crash
* Fixes issue when using Superwall with some SDK's would cause a crash (i.e. Smartlook SDK)
## 1.3.0
### Enhancements
* The existing `getPaywall` method has been deprecated and renamed to `getPaywallOrThrow`. The new `getPaywall` method now returns a `kotlin.Result` instead of throwing an exception.
* Adds a new option to `SuperwallOptions` - `passIdentifiersToPlayStore` which allows you to pass the user's identifiers (from `Superwall.instance.identify(userId: String, ...)`) to the Play Store when making a purchase. Note: When passing in identifiers to use with the play store, please make sure to follow their \[guidelines]\([https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId\(java.lang.String\)).
* Adds `Superwall.instance.confirmAllAssignments()`, which confirms assignments for all placements and returns an array of all confirmed experiment assignments. Note that the assignments may be different when a placement is registered due to changes in user, placement, or device parameters used in audience filters.
### Fixes
* Fixes issues with Paywall sometimes not displaying when returning from background
* Fixes issue with SDK crashing when WebView is not available
* Fixes issue with `SuperwallPaywallActivity` NPE
* Update visibility of internal `getPaywall` methods to `internal` to prevent misuse
## 1.2.9
### Fixes
* Fixes issues with `MODAL` presentation style and scrolling containers
* Fixes issues with `FULLSCREEN` presentation style rendering behind navigation
## 1.2.8
### Fixes
* Fixes issues with Paywall presentation styles not being properly passed
## 1.2.7
### Enhancements
* Exposes current configuration status via `Superwall.instance.configurationStatus`
### Fixes
* Fixes issues with Paywall previews not loading
## 1.2.6
### Fixes
* Fixes issue where the paywall would not show in some cases when using `minutes_since`
* Fixes issue with wrong URL being logged when a paywall fails to load
## 1.2.5
### Enhancements
* Adds a `Modifier` to `PaywallComposable` to allow for more control
* Adds a `PaywallView.setup(...)` method to allow for easy setup when using `PaywallView` directly
* Adds support for `MODAL` presentation style
### Fixes
* Fixes issue with displaying `PaywallComposable`
* Resolves issue where users would get `UninitializedPropertyAccessException` when calling `Superwall.instance`
## 1.2.4
### Enhancements
* For users who are not able to upgrade their AGP or Gradle versions, we have added a new artifact `superwall-android-agp-7` which keeps compatibility.
### Enhancements
* Fixes issue with decoding custom placements from paywalls.
## 1.2.3
### Enhancements
* Expose `placementName`, `paywallInfo` and `params` on a `custom_placement` event
## 1.2.2
### Enhancements
* Adds support for multiple paywall URLs, in case one CDN provider fails.
* `ActivityEncapsulatable` now uses a WeakReference instead of a reference
* SW-2900: Adds Superwall.instance.localeIdentifier as a convenience variable that you can use to dynamically update the locale used for evaluating rules and getting localized paywalls.
* SW-2919: Adds a `custom_placement` event that you can attach to any element in the paywall with a dictionary of parameters. When the element is tapped, the event will be tracked. The name of the placement can be used to trigger a paywall and its params used in audience filters.
* Adds support for bottom sheet presentation style (DRAWER), no animation style and default animation.
* Adds `build_id` and `cache_key` to `PaywallInfo`.
* SW-2917: Tracks a `config_attributes` event after calling `Superwall.configure`, which contains info about the configuration of the SDK. This gets tracked whenever you set the delegate.
* Adds in device attributes tracking after setting the interface style override.
* To comply with new Google Play Billing requirements we now avoid setting empty `offerToken` for one-time purchases
## 1.2.1
### Enhancements
* Adds the ability for the SDK to refresh the Superwall configuration every session start, subject to a feature flag.
* Tracks a `config_refresh` Superwall event when the configuration is refreshed.
* SW-2890: Adds `capabilities` to device attributes. This is a comma-separated list of capabilities the SDK has that you can target in audience filters. This release adds the `paywall_event_receiver` capability. This indicates that the paywall can receive transaction from the SDK.
* SW-2902: Adds `abandoned_product_id` to a `transaction_abandon` event to use in audience filters. You can use this to show a paywall if a user abandons the transaction for a specific product.
## 1.2.0
### Enhancements
* Adds DSL methods for configuring the SDK. You can now use a configuration block:
```kotlin
fun Application.configureSuperwall(
apiKey: String,
configure: SuperwallBuilder.() -> Unit,
)
```
This allows you to configure the SDK in a more idiomatic way:
```kotlin
configureSuperwall(CONSTANT_API_KEY){
options {
logging {
level = LogLevel.debug
}
paywalls {
shouldPreload = false
}
}
}
```
### Deprecations
This release includes multiple deprecations that will be removed in upcoming versions.
Most are internal and will not affect the public API, those that will are marked as such and a simple migration
path is provided. The notable ones in the public API are as follows:
* Deprecated `DebugViewControllerActivity` in favor of `DebugViewActivity`
* Deprecated `PaywallViewController` in favor of `PaywallView`
* Deprecated belonging methods:
* `viewWillAppear` in favor of `beforeViewCreated`
* `viewDidAppear` in favor of `onViewCreated`
* `viewWillDisappear` in favor of `beforeOnDestroy`
* `viewDidDisappear` in favor of `destroyed`
* `presentAlert` in favor of `showAlert`
* Deprecated `PaywallViewControllerDelegate` in favor of `PaywallViewCallback`
* Deprecated belonging methods:
* `didFinish` in favor of `onFinished`
* Deprecated `PaywallViewControllerEventDelegate` in favor of `PaywallViewEventCallback`
* Users might also note deprecation of `PaywallWebEvent.OpenedUrlInSafari` in favor of `PaywallWebEvent.OpenedUrlInChrome`
* `didFinish` in favor of `onFinished`
* In `Superwall`, the following methods were deprecated:
* `Superwall.paywallViewController` in favor of `Superwall.paywallView`
* `Superwall.eventDidOccur` argument `paywallViewController` in favor of `paywallView`
* `Superwall.dismiss` in favor of \`Superwall.presentPaywallView
* `Superwall.presentPaywallViewController` in favor of `Superwall.presentPaywallView`
* Deprecated `Paywallmanager.getPaywallViewController` in favor of `PaywallManager.getPaywallView`
* Deprecated `DebugManager.viewController` in favor of `DebugManager.view`
* Deprecated `DebugViewController` in favor of `DebugView`
* Deprecated `LogScope.debugViewController` in favor of `LogScope.debugView`
* Deprecated `PaywallPresentationRequestStatus.NoPaywallViewController` in favor of `NoPaywallView`
## 1.1.9
### Deprecations
* Deprecated configuration method `Superwall.configure(applicationContext: Context, ...)` in favor of `Superwall.configure(applicationContext: Application, ...)` to enforce type safety. The rest of the method signature remains the same.
### Fixes
* SW-2878: and it's related leaks. The `PaywallViewController` was not being properly detached when activity was stopped, causing memory leaks.
* SW-2872: Fixes issue where `deviceAttributes` event and fetching would not await for IP geo to complete.
* Fixes issues on tablet devices where the paywall would close after rotation/configuration change.
## 1.1.8
### Enhancements
* SW-2859: Adds error message to `paywallWebviewLoad_fail`.
* SW-2866: Logs error when trying to purchase a product that has failed to load.
* SW-2869: Add `Reset` event to track when `Superwall.instance.reset` is called.
* SW-2867: Prevents Geo api from being called when app is in the background
* SW-2431: Improves coroutine scope usages & threading limits
* Toolchain and dependency updates
### Fixes
* SW-2863: Fixed a `NullPointerException` some users on Android 12 & 13 would experience when calling `configure`.
## 1.1.7
### Enhancements
* SW-2805: Exposes a `presentation` property on the `PaywallInfo` object. This contains information about the presentation of the paywall.
* SW-2855: Adds `restore_start`, `restore_complete`, and `restore_fail` events.
### Fixes
* SW-2854: Fixed issue where abandoning the transaction by pressing back would prevent the user from restarting the transaction.
## 1.1.6
### Enhancements
* SW-2833: Adds support for dark mode paywall background color.
* Adds ability to target devices based on their IP address location. Use `device.ipRegion`,
`device.ipRegionCode`, `device.ipCountry`, `device.ipCity`, `device.ipContinent`, or `device.ipTimezone`.
* Adds `event_name` to the event params for use with audience filters.
### Fixes
* Fixes issue with products whose labels weren't primary/secondary/tertiary.
## 1.1.5
### Fixes
* Fixes thread safety crash when multiple threads attempted to initialize the `JavaScriptSandbox`
internally.
## 1.1.3
### Enhancements
* Tracks an `identity_alias` event whenever identify is called to alias Superwall's anonymous ID with a
developer provided id.
* Adds `setInterfaceStyle(interfaceStyle:)` which can be used to override the system interface style.
* Adds `device.interfaceStyleMode` to the device template, which can be `automatic` or `manual` if
overriding the interface style.
### Fixes
* Uses `JavascriptSandbox` when available for filter expression evaluation on a background thread
instead of running code on the main thread in a webview.
* Fixes crash where the loading spinner inside the `PaywallViewController` was being updated outside
the main thread.
## 1.1.2
### Enhancements
* Updates build.gradle configuration which means we can now upload the SDK to maven central. You no
longer need to specify our custom repo in your build.gradle to get our SDK and therefore installation
should be easier.
### Fixes
* Fixes `ConcurrentModificationException` crash that sometimes happened when identifying a user.
* Fixes crash on purchasing a free trial when using `getPaywall`.
## 1.1.1
### Fixes
* Fixes an issue loading products with offers.
## 1.1.0
### Enhancements
* SW-2768: Adds `device.regionCode` and `device.preferredRegionCode`, which returns the `regionCode`
of the locale. For example, if a locale is `en_GB`, the `regionCode` will be `GB`. You can use this
in the filters of your campaign.
* Adds support for unlimited products in a paywall.
* SW-2785: Adds internal feature flag to disable verbose events like `paywallResponseLoad_start`.
### Fixes
* SW-2732: User attributes weren't being sent on app open until identify was called. Now they are
sent every time there's a new session.
* SW-2733: Fixes issue where the spinner would still show on a paywall if a user had previously
purchased on it.
* SW-2744: Fixes issue where using the back button to dismiss a paywall presented via `getPaywall`
would call `didFinish` in the `PaywallViewControllerDelegate` with the incorrect values.
* Fixes issue where an invalid paywall background color would prevent the paywall from opening. If
this happens, it will now default to white.
* SW-2748: Exposes `viewWillAppear`, `viewDidAppear`, `viewWillDisappear` and `viewDidDisappear`
methods of `PaywallViewController` which you must call when using `getPaywall`.
* Stops `Superwall.configure` from being called multiple times.
* `getPresentationResult` now confirms assignments for holdouts.
* Gracefully handles unknown local notification types if new ones are added in the future.
* SW-2761: Fixes issue where "other" responses in paywall surveys weren't showing in the dashboard.
## 1.0.2
### Fixes
* Prevents a paywall from opening in a separate activity inside a task manager when using `taskAffinity`
within your app.
## 1.0.1
### Fixes
* Fixes serialization of `feature_gating` in `SuperwallEvents`.
* Changes the product loading so that if preloading is enabled, it makes one API request to get all
products available in paywalls. This results in fewer API requests. Also, it adds retry logic on failure.
If billing isn't available on device, the `onError` handler will be called.
## 1.0.0
### Breaking Changes
* Changes the import path for the `LogScope`, and `LogLevel`.
### Fixes
* Fixes rare thread-safety crash when sending events back to Superwall's servers.
* Calls the `onError` presentation handler block when there's no activity to present a paywall on.
* Fixes issue where the wrong product may be presented to purchase if a free trial had already been
used and you were letting Superwall handle purchases.
* Fixes `IllegalStateException` on Samsung devices when letting Superwall handle purchases.
* Keeps the text zoom of paywalls to 100% rather than respecting the accessibility settings text zoom,
which caused unexpected UI issues.
* Fixes rare `UninitializedPropertyAccessException` crash caused by a threading issue.
* Fixes crash when the user has disabled the Android System WebView.
## 1.0.0-alpha.45
### Fixes
* Fixes issue where the `paywallProductsLoad_fail` event wasn't correctly being logged. This is a
"soft fail", meaning that even though it gets logged, your paywall will still show. The error message
with the event has been updated to include all product subscription IDs that are failing to be retrieved.
## 1.0.0-alpha.44
### Fixes
* Fixes rare issue where paywall preloading was preventing paywalls from showing.
## 1.0.0-alpha.43
### Enhancements
* Adds `handleLog` to the `SuperwallDelegate`.
## 1.0.0-alpha.42
### Fixes
* Makes sure client apps use our proguard file.
## 1.0.0-alpha.41
### Fixes
* Removes need for `SCHEDULED_EXACT_ALARM` permission in manifest.
## 1.0.0-alpha.40
### Fixes
* Fixes issue presenting paywalls to users who had their device language set to Russian, Polish or
Czech.
## 1.0.0-alpha.39
### Fixes
* Adds missing `presentationSourceType` to `PaywallInfo`.
* Fixes issue where the status bar color was always dark regardless of paywall color.
* Adds `TypeToken` to proguard rules to prevent r8 from 'optimizing' our code and causing a crash.
## 1.0.0-alpha.38
### Enhancements
* SW-2682: Adds `Superwall.instance.latestPaywallInfo`, which you can use to get the `PaywallInfo` of
the most recently presented view controller.
* SW-2683: Adds `Superwall.instance.isLoggedIn`, which you can use to check if the user is logged in.
### Fixes
* Removes use of `USE_EXACT_ALARM` permission that was getting apps rejected.
* Fixes issue with scheduling notifications. The paywall wasn't waiting to schedule notifications
before dismissal so the permissions wasn't always showing.
## 1.0.0-alpha.37
### Enhancements
* SW-2684: Adds error logging if the `currentActivity` is `null` when trying to present a paywall.
### Fixes
* Fixes bug where paywalls might not present on first app install.
* Fixes bug where all `paywallResponseLoad_` events were being counted as `paywallResponseLoad_start`.
* Adds ProGuard rule to prevent `DefaultLifecycleObserver` from being removed.
## 1.0.0-alpha.36
### Enhancements
* Adds `X-Subscription-Status` header to all requests.
* Caches the last `subscriptionStatus`.
* Adds `subscriptionStatus_didChange` event that is fired whenever the subscription status changes.
* Calls the delegate method `subscriptionStatusDidChange` whenever the subscription status changes.
* SW-2676: Adds a completion block to the `configure` method.
### Fixes
* Fixes issue where the main thread was blocked when accessing some properties internally.
* SW-2679: Fixes issue where the `subscription_start` event was being fired even if a non-recurring product was
purchased.
## 1.0.0-alpha.35
### Fixes
* Fixes issue where `transaction_complete` events were being rejected by the server.
## 1.0.0-alpha.34
### Breaking Changes
* Changes `Superwall.instance.getUserAttributes()` to `Superwall.instance.userAttributes`.
* `SuperwallOptions.logging.logLevel` is now non-optional. Set it to `LogLevel.none` to prevent
logs from being printed to the console.
### Enhancements
* SW-2663: Adds `preloadAllPaywalls()` and `preloadPaywalls(eventNames:)` to be able to manually
preload paywalls.
* SW-2665: Adds `Superwall.instance.userId` so you can access the current user's id.
* SW-2668: Adds `preferredLocale` and `preferredLanguageLocale` to the device attributes for use in rules.
* Adds `Superwall.instance.logLevel` as a convenience variable to set and get the log level.
### Fixes
* SW-2664: Fixes race condition between resetting and presenting paywalls.
## 1.0.0-alpha.33
### Fixes
* Fixes issue where a user would be asked to enable notifications even if there weren't any attached
to the paywall.
## 1.0.0-alpha.32
### Enhancements
* SW-2214: Adds ability to use free trial notifications with a paywall.
* Adds `cancelAllScheduledNotifications()` to cancel any scheduled free trial notifications.
* SW-2640: Adds `computedPropertyRequests` to `PaywallInfo`.
* SW-2641: Makes `closeReason` in `PaywallInfo` non-optional.
### Fixes
* Fixes issue where thrown exceptions weren't always being caught.
## 1.0.0-alpha.31
### Enhancements
* SW-2638: Adds `Restored` to `PurchaseResult`.
* SW-2644: Adds `RestoreType` to `SuperwallEvent.TransactionRestore`.
* SW-2643: Makes `storePayment` non-optional for a `StoreTransaction`.
* SW-2642: Adds `productIdentifier` to `StorePayment`.
### Fixes
* SW-2635: Fixes crash that sometimes occurred if an app was trying to get Superwall's paywall
configuration in the background.
## 1.0.0-alpha.30
### Enhancements
* SW-2154: The SDK now includes a paywall debugger, meaning you can scan the QR code of any paywall in the
editor to preview it on device. You can change its localization, view product attributes, and view
the paywall with or without a trial.
### Fixes
* More bug fixes relating to the loading of products.
## 1.0.0-alpha.29
### Fixes
* SW-2631: Fixes issue where paywalls weren't showing if the products within them had a base plan or
offer ID set.
## 1.0.0-alpha.28
### Fixes
* SW-2615: Fixes crash on Android versions \< 8 when accessing the Android 8+ only class Period.
* SW-2616: Fixes crash where the `PaywallViewController` was sometimes being added to a new parent view before
being removed from it's existing parent view.
## 1.0.0-alpha.27
### Breaking Changes
* \#SW-2218: Changes the `PurchaseController` purchase function to `purchase(activity:productDetails:basePlanId:offerId:)`.
This adds support for purchasing offers and base plans.
### Enhancements
* \#SW-2600: Backport device attributes
* Adds support for localized paywalls.
* Paywalls are only preloaded if their associated rules can match.
* Adds status bar to full screen paywalls.
### Fixes
* Fixes issue where holdouts were still matching even if the limit set for their corresponding rules were exceeded.
* \#SW-2584: Fixes issue where prices with non-terminating decimals were causing products to fail to load.
## 1.0.0-alpha.26
### Fixes
* Additional fixes to make Google billing more robust.
* Fixes an issue that causes `transaction_complete` events to fail.
## 1.0.0-alpha.25
### Fixes
* Fixes [Google Billing Crash on Samsung devices](https://community.revenuecat.com/sdks-51/how-to-fix-crash-too-many-bind-requests-999-for-service-intent-inappbillingservice-3317).
## 1.0.0-alpha.24
### Fixes
* Fixes an issue that could cause "n/a" to be displayed on a paywall in place of the proper subscription period string.
## 1.0.0-alpha.23
### Fixes
* Fixes an issue where calling `identify` right after `configure` would hang b/c network requests need to access the user id to add to headers.
* Fixes a potential crash when loading data from disk
## 1.0.0-alpha.22
### Fixes
* Fixes threading issue
## 1.0.0-alpha.21
### Fixes
* Changes Activity to AppCompatActivity
## 1.0.0-alpha.20
### Enhancements
### Fixes
* Fixes `app_open` race condition
* Prevents calling purchase twice
* Disables interactivity during purchase
## 1.0.0-alpha.19
### Fixes
* Fixes `app_launch` event not triggering
## 1.0.0-alpha.18
### Enhancements
* Adds the ability to provide an `ActivityProvider` when configuring the SDK. This is an interface
containing the function `getCurrentActivity()`, which Superwall will use to present paywalls from.
You would typically conform an `ActivityLifecycleTracker` to this interface.
### Fixes
* Fixes a crash when storing a `List` to user attributes and if that List or a Map had a null value.
## 1.0.0-alpha.17
### Enhancements
* Adds automatic purchase controller
* Improves memory handling for webviews
* Hides the loading indicator on a paywall if transactionBackgroundView is set to NONE
## 1.0.0-alpha.14
### Enhancements
* Adds `trigger_session_id` to Superwall Events.
* Resets the scroll position of the paywall on close.
### Fixes
* Fixes issue where an invalid currency code on a device would crash the app when trying to retrieve products.
## 1.0.0-alpha.13
### Fixes
* Fixes concurrency issues when setting and retrieving values like the appUserId and seed.
## 1.0.0-alpha.11
### Fixes
* Can now use both non-recurring products and subscription products in paywalls.
* Fixes a crash issue that was caused by a lazy variable being accessed before it was initialized.
---
# Welcome
Source: https://superwall.com/docs/android
Welcome to the Superwall Android SDK documentation
## Quick Links
Get up and running with the Superwall Android SDK
Most common features and use cases
Reference the Superwall Android SDK
Guides for specific use cases
Example app for the Superwall Android SDK
Guides for troubleshooting common issues
## 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-android/issues).