# Cohorting in 3rd Party Tools Source: https://superwall.com/docs/expo/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/expo/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/expo/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: :::expo ```typescript handleSuperwallEvent(eventInfo: SuperwallEventInfo) { console.log(`handleSuperwallEvent: ${eventInfo}`); switch (eventInfo.event.type) { case EventType.appOpen: console.log("appOpen event"); break; case EventType.deviceAttributes: console.log(`deviceAttributes event: ${eventInfo.event.deviceAttributes}`); break; case EventType.paywallOpen: const paywallInfo = eventInfo.event.paywallInfo; console.log(`paywallOpen event: ${paywallInfo}`); if (paywallInfo !== null) { paywallInfo.identifier().then((identifier: string) => { console.log(`paywallInfo.identifier: ${identifier}`); }); paywallInfo.productIds().then((productIds: string[]) => { console.log(`paywallInfo.productIds: ${productIds}`); }); } break; default: break; } } ``` :::
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`: :::expo ```typescript handleSuperwallEvent(eventInfo: SuperwallEventInfo) { console.log(`handleSuperwallEvent: ${eventInfo}`); switch (eventInfo.event.type) { case EventType.appOpen: console.log("appOpen event"); break; case EventType.deviceAttributes: console.log(`deviceAttributes event: ${eventInfo.event.deviceAttributes}`); break; case EventType.paywallOpen: const paywallInfo = eventInfo.event.paywallInfo; console.log(`paywallOpen event: ${paywallInfo}`); if (paywallInfo !== null) { paywallInfo.identifier().then((identifier: string) => { console.log(`paywallInfo.identifier: ${identifier}`); }); paywallInfo.productIds().then((productIds: string[]) => { console.log(`paywallInfo.productIds: ${productIds}`); }); } break; default: break; } } ``` ::: 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/expo/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/expo/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`: :::expo ```typescript 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. --- # Game Controller Support Source: https://superwall.com/docs/expo/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/expo/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: 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/expo/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/expo/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/expo/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/expo/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. :::expo ```typescript React Native export class MyPurchaseController extends PurchaseController { // 1 async purchaseFromAppStore(productId: string): Promise { // TODO // ---- // Purchase via StoreKit, RevenueCat, Qonversion or however // you like and return a valid PurchaseResult } async purchaseFromGooglePlay( productId: string, basePlanId?: string, offerId?: string ): Promise { // TODO // ---- // Purchase via Google Billing, RevenueCat, Qonversion or however // you like and return a valid PurchaseResult } // 2 async restorePurchases(): Promise { // TODO // ---- // Restore purchases and return true if successful. } } ``` ::: 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: :::expo ```typescript React Native export default function App() { React.useEffect(() => { const apiKey = Platform.OS === "ios" ? "MY_IOS_API_KEY" : "MY_ANDROID_API_KEY" const purchaseController = new MyPurchaseController() Superwall.configure({ apiKey: apiKey, purchaseController: purchaseController, }) }, []) } ``` ::: ### 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: :::expo ```typescript React Native // When a subscription is purchased, restored, validated, expired, etc... myService.addSubscriptionStatusListener((subscriptionInfo: SubscriptionInfo) => { const entitlements = Object.keys(subscriptionInfo.entitlements.active).map((id) => ({ id, })) if (entitlements.length === 0) { Superwall.shared.setSubscriptionStatus(SubscriptionStatus.Inactive()) } else { Superwall.shared.setSubscriptionStatus( SubscriptionStatus.Active(entitlements.map((id) => new Entitlement(id))) ) } }) ``` :::
`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: :::expo ```typescript React Native Superwall.shared.subscriptionStatusEmitter.addListener("change", (status) => { switch (status.status) { case "ACTIVE": break default: break } }) ``` ::: 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). --- # Configuring Source: https://superwall.com/docs/expo/guides/configuring undefined ### 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: :::expo ```typescript const options = new SuperwallOptions() options.logging.level = LogLevel.Warn options.logging.scopes = [LogScope.PaywallPresentation, LogScope.PaywallTransactions] Superwall.configure( "MY_API_KEY", null, options: options ); // Or you can set: await Superwall.shared.setLogLevel(LogLevel.Warn) ``` ::: ### 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`: :::expo ```typescript const options = new SuperwallOptions() options.paywalls.shouldPreload = false Superwall.configure( "MY_API_KEY", null, options: options ); // Or you can set: Superwall.instance.logLevel = LogLevel.Warn ``` ::: Then, if you'd like to preload paywalls for specific placements you can use `preloadPaywalls(forPlacements:)`: :::expo ```typescript var placements = {"campaign_trigger"}; Superwall.shared.preloadPaywalls(placements); ``` ::: If you'd like to preload all paywalls you can use `preloadAllPaywalls()`: :::expo ```typescript // Coming soon ``` ::: 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`: :::expo ```typescript const options = SuperwallOptions() options.isExternalDataCollectionEnabled = false Superwall.configure( "MY_API_KEY", options: options ); ``` ::: 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`: :::expo ```typescript const options = SuperwallOptions() options.paywalls.automaticallyDismiss = false Superwall.configure( "MY_API_KEY", null, options: options ); ``` ::: 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: :::expo ```typescript const options = SuperwallOptions() options.paywalls.restoreFailed.title = "My Title" options.paywalls.restoreFailed.message = "My message"; options.paywalls.restoreFailed.closeButtonTitle = "Close"; Superwall.configure( "MY_API_KEY", null, options: options ); ``` ::: ### 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: :::expo ```typescript const options = SuperwallOptions() options.paywalls.isHapticFeedbackEnabled = false; Superwall.configure( "MY_API_KEY", null, options: options ); ``` ::: 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`: :::expo ```typescript const options = SuperwallOptions() options.paywalls.transactionBackgroundView = TransactionBackgroundView.none Superwall.configure( "MY_API_KEY", null, options: options ); ``` ::: ### 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`: :::expo ```typescript const options = SuperwallOptions() options.paywalls.shouldShowPurchaseFailureAlert = false; Superwall.configure( "MY_API_KEY", null, options: options ); ``` ::: ### 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: :::expo ```typescript const options = SuperwallOptions() options.localeIdentifier = "en_GB"; Superwall.configure( "MY_API_KEY", null, options: options ); ``` ::: 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/expo/guides/experimental-flags undefined Experimental flags in Superwall's SDK allow you to opt into features that are safe for production but are still being refined. These features may undergo naming changes or internal restructuring in future SDK versions. We expose them behind flags to give you early access while preserving flexibility for ongoing development. These flags are configured via the `SuperwallOptions` struct: ```swift let options = SuperwallOptions() options.enableExperimentalDeviceVariables = true Superwall.configure(apiKey: "my_api_key", options: options) ``` ## Available experimental flags When these flags are enabled and the user runs your app, these values become available in campaign filters. Currently, these include: **Latest Subscription Period Type (String)**: Represents whether the user is in a trial, promotional, or a similar phase. Possible values include: * `trial` * `code` * `subscription` * `promotional` * `winback` * `revoked` Represented as `latestSubscriptionPeriodType` in campaign filters. **Latest Subscription State (String)**: Represents what *state* the actual subscription is in. Possible values include: * `inGracePeriod` * `subscribed` * `expired` * `inBillingRetryPeriod` * `revoked` Represented as `latestSubscriptionState` in campaign filters. **Latest Subscription Will Auto Renew (Bool)**: If the user is set to renew or not. Either `true` or `false` Represented as `latestSubscriptionWillAutoRenew` in campaign filters. ### Detecting users who've cancelled an active trial One common use case for these flags is detecting users who've cancelled an active trial. In that case, the filter in the campaign would check for `latestSubscriptionWillAutoRenew` to be `false` and `latestSubscriptionPeriodType` to be `trial`. :::expo ### Platform Availability These variables are currently only available on **iOS**, support for Android is not yet available. ::: --- # Managing Users Source: https://superwall.com/docs/expo/guides/managing-users Learn how to manage users in your app. ## Overview The [`useUser`](/expo/sdk-reference/hooks/useUser) hook provides functions to identify users, sign them out, update their attributes, and access user and subscription status information. ## Example ```tsx import { useUser } from "expo-superwall"; import { Button, Text, View } from "react-native"; function UserManagementScreen() { const { identify, user, signOut, update, subscriptionStatus } = useUser(); const handleLogin = async () => { // Identify the user with a unique ID await identify(`user_${Date.now()}`); }; const handleSignOut = async () => { await signOut(); }; const handleUpdateUserAttributes = async () => { // Update custom user attributes await update((oldAttributes) => ({ ...oldAttributes, customProperty: "new_value", counter: (oldAttributes.counter || 0) + 1, })); }; return ( Subscription Status: {subscriptionStatus?.status ?? "unknown"} {user && User ID: {user.appUserId}} {user && User Attributes: {JSON.stringify(user, null, 2)}} } ``` **Returns:** `CustomPurchaseControllerContext | null` Returns the controller object passed to the provider, or `null` if the component is not within a `CustomPurchaseControllerProvider`. ## How It Works The `CustomPurchaseControllerProvider` listens for purchase events from the Superwall SDK using the `useSuperwallEvents` hook internally. When a purchase or restore event occurs: 1. It calls your provided `onPurchase` or `onPurchaseRestore` method 2. After your method completes, it notifies the Superwall SDK that the purchase was successful 3. Superwall then dismisses the paywall and continues with the user flow ## Integration with RevenueCat When using RevenueCat, you can integrate it with the `CustomPurchaseControllerProvider` like this: ```tsx import Purchases from 'react-native-purchases' const controller = { onPurchase: async (params) => { try { if (params.platform === "ios") { // iOS purchase logic const products = await Purchases.getProducts([params.productId]) const product = products[0] if (product) { await Purchases.purchaseStoreProduct(product) } } else { // Android purchase logic const products = await Purchases.getProducts([params.productId]) const product = products[0] if (product) { await Purchases.purchaseStoreProduct(product) } } } catch (error) { console.error("Purchase failed:", error) // Handle purchase error } }, onPurchaseRestore: async () => { try { await Purchases.restorePurchases() } catch (error) { console.error("Restore failed:", error) } }, } ``` ## Notes * The provider must wrap your app at a level where both the Superwall SDK and your purchase logic can access it * Purchase success/failure handling is automatic - you just need to perform the actual purchase * For RevenueCat integration, see the [Using RevenueCat](/docs/expo/guides/using-revenuecat) guide for complete examples --- # SuperwallLoading Source: https://superwall.com/docs/expo/sdk-reference/components/SuperwallLoading undefined `` and `` are helper components to help manage the loading state of the Superwall SDK. The `` component will render its children while the SDK is initializing. The `` component will render its children when the SDK is ready. ## Example ```tsx import { SuperwallProvider, SuperwallLoading, SuperwallLoaded, } from "expo-superwall"; import { ActivityIndicator, View, Text } from "react-native"; const API_KEY = "YOUR_SUPERWALL_API_KEY"; export default function App() { return ( {/* Your main app screen or component */} ); } function MainAppScreen() { return ( Superwall SDK is ready! {/* Rest of your app's UI */} ); } ``` --- # SuperwallProvider Source: https://superwall.com/docs/expo/sdk-reference/components/SuperwallProvider undefined `` is the root component for the Superwall SDK. It is used to initialize the SDK with your API key. ## Props ### `apiKeys` The `apiKeys` prop is an object that contains the API keys for the Superwall SDK. ## Example ```tsx import { SuperwallProvider } from "expo-superwall"; // Replace with your actual Superwall API key export default function App() { return ( {/* Your app content goes here */} ); } ``` --- # usePlacement Source: https://superwall.com/docs/expo/sdk-reference/hooks/usePlacement A React hook that registers a placement so it can remotely trigger a paywall, gate feature access, and expose paywall-lifecycle state. ## 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 while exposing the paywall lifecycle as React state. ## Signature ```ts function usePlacement( callbacks?: usePlacementCallbacks ): { /** Registers the placement and potentially presents a paywall. */ registerPlacement: (args: RegisterPlacementArgs) => Promise /** Current paywall-lifecycle state. */ state: PaywallState } ``` ## Parameters | Name | Type | Description | | ----------- | ------------------------ | -------------------------------------------------------------------- | | `callbacks` | `usePlacementCallbacks?` | Optional callbacks that fire at each stage of the paywall lifecycle. | ### usePlacementCallbacks | Name | Type | Description | | ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | | `onPresent` | `(paywallInfo: PaywallInfo) => void` | Called when a paywall is presented. | | `onDismiss` | `(paywallInfo: PaywallInfo, result: PaywallResult) => void` | Called when a paywall is dismissed. | | `onSkip` | `(reason: PaywallSkippedReason) => void` | Called when presentation is skipped (e.g., hold-out, no audience match). | | `onError` | `(error: string) => void` | Called when the paywall fails to present or another SDK error occurs. | ### RegisterPlacementArgs | Name | Type | Description | | ----------- | ---------------------- | -------------------------------------------------------------------------------- | | `placement` | `string` | Placement name as defined on the Superwall Dashboard. | | `params` | `Record?` | Optional parameters passed to the placement. | | `feature` | `() => void` | Function executed **only** if no paywall is shown (the user is allowed through). | ## Returns / State The hook returns an object with: • `registerPlacement` — `(args: RegisterPlacementArgs) => Promise`\ • `state` — `PaywallState` `PaywallState` union: * `{ status: "idle" }` * `{ status: "presented"; paywallInfo: PaywallInfo }` * `{ status: "dismissed"; result: PaywallResult }` * `{ status: "skipped"; reason: PaywallSkippedReason }` * `{ status: "error"; error: string }` ## Usage ```tsx import { Button, Text } from "react-native" import { usePlacement } from "superwall-expo" export default function PremiumButton() { const { registerPlacement, state } = usePlacement({ onPresent: (info) => console.log("Paywall presented:", info.name), onDismiss: (_, result) => console.log("Paywall dismissed:", result.type), onSkip: (reason) => console.log("Paywall skipped:", reason.type), onError: (error) => console.error("Paywall error:", error), }) const unlockFeature = async () => { await registerPlacement({ placement: "MyFeaturePlacement", feature: () => { // User was allowed through without a paywall navigateToPremiumFeature() }, }) } return ( <>