From ffc124c5580396ba1c8f4a3c1ecb0f0545f73fa9 Mon Sep 17 00:00:00 2001 From: Leonardo Bilck Date: Fri, 4 Sep 2026 00:44:26 -0300 Subject: [PATCH] Add PaywallOptions.HideAndroidSystemBars to run paywalls immersive on Android The paywall is the SDK's own Activity, so a host app that runs full-screen (immersive) gets the status and navigation bars back the moment a paywall opens. superwall-android's edge-to-edge handling then pads only the bottom system-bar inset; in landscape the 3-button navigation bar sits on a side and covers paywall content there. The new option is bridge-level (no native counterpart): when true, the Android bridge registers ActivityLifecycleCallbacks once at configure and, whenever SuperwallPaywallActivity is created or resumed, hides the system bars with the sticky-immersive behaviour (a swipe from the edge shows them transiently). The activity is matched by class name so an upstream rename degrades to a no-op. Default false; ignored on iOS. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 5 +++ .../sdk/unity/SuperwallUnityBridge.kt | 45 +++++++++++++++++++ Runtime/Superwall.cs | 1 + Runtime/SuperwallOptions.cs | 9 ++++ 4 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffc0bc6..9d65bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this package will be documented in this file. +## [Unreleased] + +### Added +* `PaywallOptions.HideAndroidSystemBars` (default `false`). Android only: hides the status and navigation bars while `SuperwallPaywallActivity` is in the foreground, sticky-immersive, so a paywall matches an app that already runs full-screen. The paywall is its own Activity, so the host app's immersive flags never reached it, and the SDK's edge-to-edge handling pads only the bottom inset — in landscape the 3-button navigation bar sits on a side and covered paywall content. Ignored on iOS. + ## [0.2.6] ### Dependencies diff --git a/Plugins/Android/SuperwallSDK.androidlib/src/main/kotlin/com/superwall/sdk/unity/SuperwallUnityBridge.kt b/Plugins/Android/SuperwallSDK.androidlib/src/main/kotlin/com/superwall/sdk/unity/SuperwallUnityBridge.kt index 619ea2f..ade7291 100644 --- a/Plugins/Android/SuperwallSDK.androidlib/src/main/kotlin/com/superwall/sdk/unity/SuperwallUnityBridge.kt +++ b/Plugins/Android/SuperwallSDK.androidlib/src/main/kotlin/com/superwall/sdk/unity/SuperwallUnityBridge.kt @@ -1,7 +1,12 @@ package com.superwall.sdk.unity import android.app.Activity +import android.app.Application import android.net.Uri +import android.os.Bundle +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import com.unity3d.player.UnityPlayer import com.superwall.sdk.Superwall import com.superwall.sdk.config.options.SuperwallOptions @@ -70,11 +75,49 @@ class SuperwallUnityBridge { is SubscriptionStatus.Unknown -> put("type", "unknown") } } + + // Bridge-level option with no native counterpart; see PaywallOptions.HideAndroidSystemBars in C#. + // The paywall is its own Activity, so the host's immersive flags do not reach it, and the SDK's + // edge-to-edge handling pads only the bottom inset - in landscape the navigation bar sits on a side + // and covers paywall content. Matched by class name rather than type so a rename upstream degrades + // to "no-op" instead of a compile error. + private const val PAYWALL_ACTIVITY_CLASS_NAME = "com.superwall.sdk.paywall.view.SuperwallPaywallActivity" + private var hideAndroidSystemBars = false + private var paywallWindowCallbacksRegistered = false + + private fun registerPaywallWindowCallbacks(application: Application) { + if (paywallWindowCallbacksRegistered) return + paywallWindowCallbacksRegistered = true + application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { + // Created AND resumed: created catches the first frame, resumed re-applies after a system + // dialog or the store sheet has shown the bars again. + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = applySystemBarsPolicy(activity) + override fun onActivityResumed(activity: Activity) = applySystemBarsPolicy(activity) + override fun onActivityStarted(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + }) + } + + private fun applySystemBarsPolicy(activity: Activity) { + if (!hideAndroidSystemBars) return + if (activity.javaClass.name != PAYWALL_ACTIVITY_CLASS_NAME) return + val window = activity.window ?: return + WindowCompat.setDecorFitsSystemWindows(window, false) + WindowInsetsControllerCompat(window, window.decorView).apply { + // Sticky immersive: a swipe from the edge shows the bars transiently, then they hide again. + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + hide(WindowInsetsCompat.Type.systemBars()) + } + } } fun configure(apiKey: String, optionsJson: String?, hasPurchaseController: Boolean, completionCallbackId: String?) { val activity = UnityPlayer.currentActivity ?: return val options = if (optionsJson != null) parseOptions(optionsJson) else null + registerPaywallWindowCallbacks(activity.application) val unityActivityProvider = object : com.superwall.sdk.misc.ActivityProvider { override fun getCurrentActivity(): Activity? = UnityPlayer.currentActivity @@ -840,6 +883,8 @@ class SuperwallUnityBridge { } if (json.has("paywalls")) { val paywallsJson = json.getJSONObject("paywalls") + // Bridge-level, not a SuperwallOptions field: consumed by applySystemBarsPolicy. + if (paywallsJson.has("hideAndroidSystemBars")) hideAndroidSystemBars = paywallsJson.getBoolean("hideAndroidSystemBars") if (paywallsJson.has("isHapticFeedbackEnabled")) paywalls.isHapticFeedbackEnabled = paywallsJson.getBoolean("isHapticFeedbackEnabled") if (paywallsJson.has("shouldShowPurchaseFailureAlert")) paywalls.shouldShowPurchaseFailureAlert = paywallsJson.getBoolean("shouldShowPurchaseFailureAlert") if (paywallsJson.has("shouldPreload")) paywalls.shouldPreload = paywallsJson.getBoolean("shouldPreload") diff --git a/Runtime/Superwall.cs b/Runtime/Superwall.cs index 0cb96d6..7d2643c 100644 --- a/Runtime/Superwall.cs +++ b/Runtime/Superwall.cs @@ -804,6 +804,7 @@ private static string SerializeOptions(SuperwallOptions options) paywalls["shouldShowWebRestorationAlert"] = options.Paywalls.ShouldShowWebRestorationAlert; paywalls["transactionBackgroundView"] = options.Paywalls.TransactionBackgroundView.ToString(); paywalls["shouldShowWebPurchaseConfirmationAlert"] = options.Paywalls.ShouldShowWebPurchaseConfirmationAlert; + paywalls["hideAndroidSystemBars"] = options.Paywalls.HideAndroidSystemBars; if (options.Paywalls.RestoreFailed != null) { diff --git a/Runtime/SuperwallOptions.cs b/Runtime/SuperwallOptions.cs index 639163a..cfa8c20 100644 --- a/Runtime/SuperwallOptions.cs +++ b/Runtime/SuperwallOptions.cs @@ -34,6 +34,15 @@ public class PaywallOptions public bool ShouldShowWebPurchaseConfirmationAlert = false; public bool UseCachedTemplates = false; public float? TimeoutAfter = null; + + /// + /// Android only. Hides the status and navigation bars while a Superwall paywall Activity is in the + /// foreground (sticky immersive: a swipe from the edge shows them transiently), so a paywall matches an + /// app that already runs full-screen. Without it the paywall Activity is edge-to-edge but pads only the + /// bottom system-bar inset, so in landscape the 3-button navigation bar covers paywall content on one + /// side. Ignored on iOS. Default false to keep the platform default for existing apps. + /// + public bool HideAndroidSystemBars = false; } [Serializable]