# 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": ![](/images/3pa_cp_2.jpeg) You could create a custom placement [tap behavior](/paywall-editor-styling-elements#tap-behaviors) which fires when a segment is tapped: ![](/images/3pa_cp_1.jpeg) Then, you can listen for this placement and forward it to your analytics service: ```swift Swift extension SuperwallService: SuperwallDelegate { func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) { switch eventInfo.event { case let .customPlacement(name, params, paywallInfo): // Prints out didTapPro or didTapStandard print("\(name) - \(params) - \(paywallInfo)") MyAnalyticsService.shared.send(event: name, params: params) default: print("Default event: \(eventInfo.event.description)") } } } ``` For a walkthrough example, check out this [video on YouTube](https://youtu.be/4rM1rGRqDL0). --- # 3rd Party Analytics Source: https://superwall.com/docs/android/guides/3rd-party-analytics/index 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). --- # 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_timeout` | When the loading of a paywall's webpage times out. | 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` | --- # 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. --- # 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. --- # 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! ::: --- # 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: ![](/images/om_transactions.png) This is useful if you are using Superwall solely for revenue tracking, and you're making purchases using frameworks like StoreKit or Google Play Billing Library directly. Observer mode will also properly link user identifiers to transactions. To enable observer mode, set it using `SuperwallOptions` when configuring the SDK: :::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). --- # 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. --- # 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). --- # 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() } } } } } ``` --- # 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). --- # 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 } } } ``` ::: ### 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. --- # Experimental Flags Source: https://superwall.com/docs/android/guides/experimental-flags undefined Support for Experimental Flags in Android is not yet available. --- # 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.0.0" ``` ## 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. ![](/images/camp-presentation-conditions.png) ## 6. Check out the full change log You can view this on [our GitHub page](https://github.com/superwall/Superwall-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 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 can integrate RevenueCat with Superwall using several approaches: ## Class-Based Integration You can integrate RevenueCat with Superwall using purchase controllers: 1. **Using a purchase controller:** Use this route if you want to maintain control over purchasing logic and code. 2. **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). ### 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 `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). --- # 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: [![Install MCP Server](/images/cursor-mcp-install-light.svg)](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 ``` ## 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) | To minimize token use, we recommend using the files specific to your SDK. --- # Web Checkout Source: https://superwall.com/docs/android/guides/web-checkout/index Integrate Superwall web checkout with your iOS app for seamless cross-platform subscriptions ## Dashboard Setup First, you need to [set up Web Checkout in the dashboard](/dashboard/web-checkout/web-checkout-overview). ## 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](/dashboard/web-checkout/web-checkout-testing-purchases) 2. [Managing Memberships](/dashboard/web-checkout/web-checkout-managing-memberships) ## FAQ [Web Checkout FAQ](/dashboard/web-checkout/web-checkout-faq) --- # 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 StoreKit in your PurchaseController, you'll need to merge the web entitlements with the device entitlements before setting the subscription status. Here's an example of how you might do this: ```swift func syncSubscriptionStatus() async { var products: Set = [] // Get the device entitlements for await verificationResult in Transaction.currentEntitlements { switch verificationResult { case .verified(let transaction): products.insert(transaction.productID) case .unverified: break } } let storeProducts = await Superwall.shared.products(for: products) let deviceEntitlements = Set(storeProducts.flatMap { $0.entitlements }) // Get the web entitlements from Superwall let webEntitlements = Superwall.shared.entitlements.web // Merge the two sets of entitlements let allEntitlements = deviceEntitlements.union(webEntitlements) await MainActor.run { Superwall.shared.subscriptionStatus = .active(allEntitlements) } } ``` In addition to syncing the subscription status when purchasing and restoring, you'll need to sync it whenever `didRedeemLink(result:)` is called: ```swift final class Delegate: SuperwallDelegate { func didRedeemLink(result: RedemptionResult) { Task { await syncSubscriptionStatus() } } } ``` ### Refreshing of web entitlements If you aren't using a Purchase Controller, the SDK will refresh the web entitlements every 24 hours. ### Redeeming while a paywall is open If a redeem event occurs when a paywall is open, the SDK will track that as a restore event and the paywall will close. --- # 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. Whether you’re showing a checkout page in Safari or using the In-App Browser, the Superwall SDK relies on deep links to redirect back to your app. #### Prerequisites 1. [Configuring Stripe Keys and Settings](/web-checkout-configuring-stripe-keys-and-settings) 2. [Deep Links](/in-app-paywall-previews) Here, we'll focus on how to handle the user experience when the user has been redirected back to your app after a web purchase, using `SuperwallDelegate` methods. 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) ## willRedeemLink When your app opens via the deep link, we will call the delegate method `willRedeemLink()` before making a network call to redeem the code. At this point, you might wish to display a loading indicator in your app so the user knows that the purchase is being redeemed. ```swift func willRedeemLink() { ToastView.show(message: "Activating...", showActivityIndicator: true) } ``` To present your own loading UI on top of the paywall, you can access the view controller of the paywall via `Superwall.shared.presentedViewController`. You can manually dismiss the paywall here, but note that the completion block of the original `register` call won't be triggered. The paywall will be dismissed automatically when the `didRedeemLink` method is called. ## didRedeemLink After receiving a response from the network, we will call `didRedeemLink(result:)` with the result of redeeming the code. This is an enum that has the following cases: * `success(code: String, redemptionInfo: RedemptionInfo)`: The redemption succeeded and `redemptionInfo` contains information about the redeemed code. * `error(code: String, error: ErrorInfo)`: An error occurred while redeeming. You can check the error message via the `error` parameter. * `expiredCode(code: String, expired: ExpiredCodeInfo)`: The code expired and `ExpiredCodeInfo` contains information about whether a redemption email has been resent and an optional obfuscated email address that the redemption email was sent to. * `invalidCode(code: String)`: The code that was redeemed was invalid. * `expiredSubscription(code: String, redemptionInfo: RedemptionInfo)`: The subscription that the code redeemed has expired. On network failure, the SDK will retry up to 6 times before returning an `error` `RedemptionResult` in `didRedeemLink(result:)`. Here, you should remove any loading UI you added in `willRedeemLink` and show a message to the user based on the result. If a paywall is presented, it will be dismissed automatically. ```swift func didRedeemLink(result: RedemptionResult) { switch result { case .expiredCode(let code, let expiredInfo): ToastView.show(message: "Expired Link", systemImageName: "exclamationmark.square.fill") print("[!] code expired", code, expiredInfo) break case .error(let code, let error): ToastView.show(message: error.message, systemImageName: "exclamationmark.square.fill") print("[!] error", code, error) break case .expiredSubscription(let code, let redemptionInfo): ToastView.show(message: "Expired Subscription", systemImageName: "exclamationmark.square.fill") print("[!] expired subscription", code, redemptionInfo) break case .invalidCode(let code): ToastView.show(message: "Invalid Link", systemImageName: "exclamationmark.square.fill") print("[!] invalid code", code) break case .success(_, let redemptionInfo): if let email = redemptionInfo.purchaserInfo.email { Superwall.shared.setUserAttributes(["email": email]) ToastView.show(message: email, systemImageName: "person.circle.fill") } else { ToastView.show(message: "Welcome!", systemImageName: "person.circle.fill") } break } } ``` --- # 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. If you're using your own `PurchaseController`, you should follow our [Redeeming In-App](/web-checkout-linking-membership-to-iOS-app) guide. ### Using a PurchaseController with RevenueCat If you're using RevenueCat, you'll need to follow [steps 1 to 4 in their guide](https://www.revenuecat.com/docs/web/integrations/stripe) to set up Stripe with RevenueCat. Then, you'll need to associate the RevenueCat customer with the Stripe subscription IDs returned from redeeming the code. You can do this by extracting the ids from the `RedemptionResult` and sending them to RevenueCat's API by using the `didRedeemLink(result:)` delegate method: ```swift import Foundation import RevenueCat final class Delegate: SuperwallDelegate { // The user tapped on a deep link to redeem a code func willRedeemLink() { print("[!] willRedeemLink") // Optionally show a loading indicator here } // Superwall received a redemption result and validated the purchase with Stripe. func didRedeemLink(result: RedemptionResult) { print("[!] didRedeemLink", result) // Send Stripe IDs to RevenueCat to link purchases to the customer // Get a list of subscription ids tied to the customer. guard let stripeSubscriptionIds = result.stripeSubscriptionIds else { return } guard let url = URL(string: "https://api.revenuecat.com/v1/receipts") else { return } let revenueCatStripePublicAPIKey = "strp....." // replace with your RevenueCat Stripe Public API Key let appUserId = Purchases.shared.appUserID // In the background... Task.detached { await withTaskGroup(of: Void.self) { group in // For each subscription id, link it to the user in RevenueCat for stripeSubscriptionId in stripeSubscriptionIds { group.addTask { var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("stripe", forHTTPHeaderField: "X-Platform") request.setValue("Bearer \(revenueCatStripePublicAPIKey)", forHTTPHeaderField: "Authorization") do { request.httpBody = try JSONEncoder().encode([ "app_user_id": appUserId, "fetch_token": stripeSubscriptionId ]) let (data, _) = try await URLSession.shared.data(for: request) let json = try JSONSerialization.jsonObject(with: data, options: []) print("[!] Success: linked \(stripeSubscriptionId) to user \(appUserId)", json) } catch { print("[!] Error: unable to link \(stripeSubscriptionId) to user \(appUserId)", error) } } } } /// After all network calls complete, invalidate the cache without switching to the main thread. Purchases.shared.getCustomerInfo(fetchPolicy: .fetchCurrent) { customerInfo, error in /// If you're using `Purchases.shared.customerInfoStream`, or keeping Superwall Entitlements in sync /// via RevenueCat's `PurchasesDelegate` methods, you don't need to do anything here. Those methods will be /// called automatically when this call fetches the most up to customer info, ignoring any local caches. /// Otherwise, if you're manually calling `Purchases.shared.getCustomerInfo` to keep Superwall's entitlements /// in sync, you should use the newly updated customer info here to do so. } /// You could always access web entitlements here as well /// `let webEntitlements = Superwall.shared.entitlements.web` // After all network calls complete... await MainActor.run { // Perform UI updates on the main thread, like letting the user know their subscription was redeemed } } } } ``` 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. --- # Welcome Source: https://superwall.com/docs/android/index 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). --- # 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**: ![](/images/810eaba-small-Screenshot_2023-04-25_at_11.51.13.png) ### Initialize Superwall in your app Begin by editing your main Application entrypoint. Depending on the platform this could be `AppDelegate.swift` or `SceneDelegate.swift` for iOS, `MainApplication.kt` for Android, `main.dart` in Flutter, or `App.tsx` for React Native: :::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. ::: --- # 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: --- # 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) } } } ``` ::: ## Previewing Paywalls Next, build and run your app on your phone. Then, head to the Superwall Dashboard. Click on **Settings** from the Dashboard panel on the left, then select **General**: ![](/images/c252198-image.png) With the **General** tab selected, type your custom URL scheme, without slashes, into the **Apple Custom URL Scheme** field: ![](/images/6b3f37e-image.png) Next, open your paywall from the dashboard and click **Preview**. You'll see a QR code appear in a pop-up: ![](/images/2.png)
![](/images/3.png) On your device, scan this QR code. You can do this via Apple's Camera app. This will take you to a paywall viewer within your app, where you can preview all your paywalls in different configurations. ## Using Deep Links to Present Paywalls Deep links can also be used as a placement in a campaign to present paywalls. Simply add `deepLink_open` as an placement, and the URL parameters of the deep link can be used as parameters! You can also use custom placements for this purpose. [Read this doc](/presenting-paywalls-from-one-another) for examples of both. --- # 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). ![](/images/installation/build-gradle-app.png) ```groovy build.gradle implementation "com.superwall.sdk:superwall-android:2.2.2" ``` ```kotlin build.gradle.kts implementation("com.superwall.sdk:superwall-android:2.2.2") ``` ```toml libs.version.toml [libraries] superwall-android = { group = "com.superwall.sdk", name = "superwall-android", version = "2.2.2" } // And in your build.gradle.kts dependencies { implementation(libs.superwall.android) } ``` Make sure to run **Sync Now** to force Android Studio to update. ![](/images/installation/gradle-sync-now.png)
Go to your `AndroidManifest.xml` and add the following permissions: ![](/images/installation/manifest-permissions.png) ```xml AndroidManifest.xml ``` Then add our Activity to your `AndroidManifest.xml`: ![](/images/installation/manifest-activity.png) ```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 👇 --- # 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. 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 ) 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). --- # Tracking Subscription State Source: https://superwall.com/docs/android/quickstart/tracking-subscription-state undefined Superwall tracks the subscription state of a user for you. So, you don't need to add in extra logic for this. However, there are times in your app where you simply want to know if a user is on a paid plan or not. In your app's models, you might wish to set a flag representing whether or not a user is on a paid subscription: ```swift @Observable class UserData { var isPaidUser: Bool = false } ``` ### Using subscription status You can do this by observing the `subscriptionStatus` property on `Superwall.shared`. This property is an enum that represents the user's subscription status: ```swift switch Superwall.shared.subscriptionStatus { case .active(let entitlements): logger.info("User has active entitlements: \(entitlements)") userData.isPaidUser = true case .inactive: logger.info("User is free plan.") userData.isPaidUser = false case .unknown: logger.info("User is inactive.") userData.isPaidUser = false } ``` One natural way to tie the logic of your model together with Superwall's subscription status is by having your own model conform to the [Superwall Delegate](/using-superwall-delegate): ```swift @Observable class UserData { var isPaidUser: Bool = false } extension UserData: SuperwallDelegate { // MARK: Superwall Delegate func subscriptionStatusDidChange(from oldValue: SubscriptionStatus, to newValue: SubscriptionStatus) { switch newValue { case .active(_): // If you're using more than one entitlement, you can check which one is active here. // This example just assumes one is being used. logger.info("User is pro plan.") self.isPaidUser = true case .inactive: logger.info("User is free plan.") self.isPaidUser = false case .unknown: logger.info("User is free plan.") self.isPaidUser = false } } } ``` Another shorthand way to check? The `isActive` flag, which returns true if any entitlement is active: ```swift if Superwall.shared.subscriptionStatus.isActive { userData.isPaidUser = true } ``` ### Superwall checks subscription status for you Remember that the Superwall SDK uses its [audience filters](/campaigns-audience#matching-to-entitlements) for a similar purpose. You generally don't need to wrap your calls registering placements around `if` statements checking if a user is on a paid plan, like this: ```swift // Unnecessary if !Superwall.shared.subscriptionStatus.isActive { Superwall.shared.register(placement: "campaign_trigger") } ``` In your audience filters, you can specify whether or not the subscription state should be considered... ![](/images/entitlementCheck.png) ...which eliminates the needs for code like the above. This keeps you code base cleaner, and the responsibility of "Should this paywall show" within the Superwall campaign platform as it was designed. --- # 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 Note that for Android apps, if you want the `userId` passed to the Play Store when making purchases, you'll also need to set `passIdentifiersToPlayStore` via `SuperwallOptions`. Be aware of Google's rules that the `userId` must not contain any personally identifiable information, otherwise the purchase could [be rejected](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId). ::: :::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 | 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.\ Make sure any `userId` you pass to `identify` is a valid UUID string, as Apple requires `appAccountToken` values to follow the UUID format. --- # 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 | Method | Parameters | Return Type | Description | | ------------------ | --------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `purchase` | `activity: Activity`, `product: StoreProduct` | `PurchaseResult` | Called when user initiates purchasing. Implement your purchase logic here. Activity is needed for Google Play Billing. | | `restorePurchases` | None | `RestorationResult` | Called when user initiates restore. Implement your restore logic here. | ## 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). --- # 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() )) ``` Set delegate: ```kotlin Superwall.instance.delegate = this ``` Java usage: ```java // Access the instance Superwall.getInstance().register("feature_access", () -> { // Feature code here }); // Set user identity Superwall.getInstance().identify("user123"); ``` --- # 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? ) {} } ``` ```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: | Method | Parameters | Description | | ----------------------------- | ------------- | --------------------------------------------------------------------------------- | | `subscriptionStatusDidChange` | `from`, `to` | Called when subscription status changes. | | `handleSuperwallEvent` | `eventInfo` | Called for all internal analytics events. Use for tracking in your own analytics. | | `handleCustomPaywallAction` | `name` | Called when user taps elements with `data-pw-custom` tags. | | `willPresentPaywall` | `paywallInfo` | Called before paywall presentation. | | `didPresentPaywall` | `paywallInfo` | Called after paywall presentation. | | `willDismissPaywall` | `paywallInfo` | Called before paywall dismissal. | | `didDismissPaywall` | `paywallInfo` | Called after paywall dismissal. | ## 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() } ``` 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); } } ``` --- # 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. --- # 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 and behavior for each environment. ## Purpose Configures various aspects of Superwall behavior including paywall presentation, networking, and logging. ## Signature ```kotlin class SuperwallOptions { var paywalls: PaywallOptions = PaywallOptions() var networkEnvironment: NetworkEnvironment = NetworkEnvironment.RELEASE var logging: LoggingOptions = LoggingOptions() var localeIdentifier: String? = null } ``` ```java // Java public class SuperwallOptions { public PaywallOptions paywalls = new PaywallOptions(); public NetworkEnvironment networkEnvironment = NetworkEnvironment.RELEASE; public LoggingOptions logging = new LoggingOptions(); public String localeIdentifier = null; } ``` ## Parameters | Property | Type | Description | | -------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `paywalls` | `PaywallOptions` | Configuration for paywall appearance and behavior. | | `networkEnvironment` | `NetworkEnvironment` | Network environment (`.RELEASE`, `.RELEASE_CANDIDATE`, `.DEVELOPER`, `.CUSTOM(String)`). **Use only if instructed by Superwall team.** | | `logging` | `LoggingOptions` | Logging configuration including level and scopes. | | `localeIdentifier` | `String?` | Override locale for paywall localization (e.g., "en\_GB"). | ## Returns / State This is a configuration object used when calling [`configure()`](/android/sdk-reference/configure). ## Usage Basic options setup: ```kotlin val options = SuperwallOptions().apply { // Configure paywall behavior paywalls.shouldShowPurchaseFailureAlert = false // Configure logging logging.level = LogLevel.WARN // Set locale for testing localeIdentifier = "en_GB" } // Use with configure Superwall.configure( application = this, apiKey = "pk_your_api_key", options = options ) ``` --- # 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 | | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `placement` | `String` | The name of the placement as defined on the Superwall dashboard. | | `params` | `Map?` | Optional parameters to pass with your placement for audience filters. Keys beginning with `$` are reserved and will be dropped. | | `overrides` | `PaywallOverrides?` | Optional overrides for products and presentation style. | | `delegate` | `PaywallViewCallback` | A delegate to handle user interactions with the retrieved PaywallView. | | `activity` | `Activity` | The activity context required for the PaywallView. | ## 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() } ) } ``` --- # 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 | | -------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | `SubscriptionStatus` | The subscription status to set. Can be `SubscriptionStatus.Unknown`, `SubscriptionStatus.Active(entitlements)`, or `SubscriptionStatus.Inactive`. | ## 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 ); ``` --- # 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. ## Signature ```swift iOS public static func configure( apiKey: String, purchaseController: PurchaseController? = nil, options: SuperwallOptions? = nil, completion: (() -> Void)? = nil ) -> Superwall ``` ```kotlin Android public fun configure( application: Application, apiKey: String, purchaseController: PurchaseController? = null, options: SuperwallOptions? = null, completion: (() -> Unit)? = null ) ``` ```dart Flutter static Future configure( String apiKey, { SuperwallOptions? options, PurchaseController? purchaseController, }) ``` ```typescript React Native static configure( apiKey: string, purchaseController?: PurchaseController, options?: SuperwallOptions ): Promise ``` ## Parameters | Name | Type | Description | | -------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `application` | `Application` | Your Android Application instance, required for SDK initialization and lifecycle management. | | `apiKey` | `String` | Your Public API Key from the Superwall dashboard settings. | | `purchaseController` | `PurchaseController?` | Optional object for handling all subscription-related logic yourself. If `null`, Superwall handles subscription logic. Defaults to `null`. | | `options` | `SuperwallOptions?` | Optional configuration object for customizing paywall appearance and behavior. See [`SuperwallOptions`](/android/sdk-reference/SuperwallOptions) for details. Defaults to `null`. | | `completion` | `(() -> Unit)?` | Optional completion handler called when Superwall finishes configuring. Defaults to `null`. | ## Returns / State Configures the Superwall instance which is accessible via [`Superwall.instance`](/android/sdk-reference/Superwall). ## Usage ```swift iOS // AppDelegate or SceneDelegate func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Superwall.configure(apiKey: "pk_your_api_key") return true } ``` ```kotlin Android class MyApplication : Application() { override fun onCreate() { super.onCreate() Superwall.configure( application = this, apiKey = "pk_your_api_key" ) } } ``` ```dart Flutter void main() async { WidgetsFlutterBinding.ensureInitialized(); await Superwall.configure("pk_your_api_key"); runApp(MyApp()); } ``` ```typescript React Native // App.tsx export default function App() { useEffect(() => { Superwall.configure("pk_your_api_key"); }, []); return ; } ``` With custom options: ```kotlin class MyApplication : Application() { override fun onCreate() { super.onCreate() val options = SuperwallOptions().apply { paywalls.shouldShowPurchaseFailureAlert = false } Superwall.configure( application = 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 | | ----- | -------- | -------------------------------------------------- | | `url` | `String` | The deep link URL to process for paywall triggers. | ## 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") ``` --- # 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 | | --------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `userId` | `String` | Your user's unique identifier, as defined by your backend system. | | `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. Defaults to `null`. | ## 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); ``` --- # Overview Source: https://superwall.com/docs/android/sdk-reference/index 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). --- # 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 | | ----------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `placement` | `String` | The name of the placement you wish to register. | | `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. Defaults to `null`. | | `handler` | `PaywallPresentationHandler?` | A handler whose functions provide status updates for the paywall lifecycle. Defaults to `null`. | | `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**. | ## 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 ) ``` --- # 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 | | ------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `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. | ## 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); ``` --- # 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")) ); ``` --- # 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()); } ``` --- # Android Source: https://superwall.com/docs/android The long-awaited Superwall for Android is now available! Here's how to get started! ### Quickstart Follow [this guide](/creating-applications) to create a new android app. Follow [this guide](/installation-via-gradle) to install the SDK. Follow [this guide](/configuring-the-sdk) to configure the SDK You're all set! 🎉 Reach out to us on Intercom or by [email](mailto\:team@superwall.com) For any feedback, email [team@superwall.com](mailto\:team@superwall.com) *** #### Known Issues * [Presentation Style](/paywall-editor-settings#presentation-style) (right now only full screen) #### Next Up! Once we're done fixing up any initial issues we'll move on to improving on the version we've delivered. Including supporting Google Upgrade & Downgrade flow.