-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Environment
- Superwall Android SDK version: 2.6.8
- Android version: API 35 (Android 15)
- Device: Emulator (Medium Phone API 35)
- Kotlin version: 2.2.10
- Jetpack Compose: Yes (compose-bom 2024.12.01)
Description
When a user taps an external link (e.g., Terms of Service, Privacy Policy) in the paywall, Chrome Custom Tabs opens. Upon returning to the app, the paywall is unexpectedly auto-dismissed, AND Superwall.instance.isPaywallPresented remains stuck at true, preventing any subsequent paywall presentations.
This is actually two bugs:
-
Primary bug: Paywall should NOT auto-dismiss when returning from external links. The expected behavior is that the paywall remains visible when the user returns from Chrome Custom Tabs.
-
Secondary bug: When the paywall is dismissed (incorrectly),
isPaywallPresentedstaystrueinstead of resetting tofalse. This causes subsequentregister()calls to fail with error:SWPresentationError: 102, Paywall Already Presented.
Steps to Reproduce
- Configure Superwall with a paywall that has external links (Terms/Privacy)
- Call
Superwall.instance.register(placement = "your_placement")to present the paywall - Paywall presents successfully
- Tap on an external link in the paywall (e.g., "Terms of Service")
- Chrome Custom Tabs opens and displays the external page
- Press the back button or close Chrome Custom Tabs to return to the app
- Observe: Paywall is gone (auto-dismissed unexpectedly)
- Try to trigger the paywall again via
register() - Observe: Paywall does not present, error callback fires
Expected Behavior
- After returning from Chrome Custom Tabs, the paywall should still be visible
- User should be able to dismiss the paywall manually
- If paywall IS dismissed,
isPaywallPresentedshould reset tofalse - Subsequent
register()calls should work normally
Actual Behavior
- Paywall auto-dismisses when returning from Chrome Custom Tabs
isPaywallPresentedremainstrueindefinitely- Subsequent
register()calls fail with "Paywall Already Presented" error - Only way to recover is to restart the app
Logcat Evidence
First paywall presentation (successful):
D RootScreen: isPaywallPresented: false
D RootScreen: Calling register() with placement: onboarding_completed
D RootScreen: onPresent: Paywall presented - regular-paywall-new-7cf9-2026-01-18
After returning from Chrome Custom Tabs and trying again:
D RootScreen: isPaywallPresented: true
D RootScreen: Calling register() with placement: onboarding_completed
E RootScreen: onError: Paywall error - java.lang.RuntimeException: SWPresentationError: 102, Paywall Already Presented - You can only present one paywall at a time.
Retry attempts show state never resets:
D RootScreen: Attempt 1: isPaywallPresented = true
D RootScreen: Attempt 2: isPaywallPresented = true
D RootScreen: Attempt 3: isPaywallPresented = true
D RootScreen: Attempt 4: isPaywallPresented = true
D RootScreen: Attempt 5: isPaywallPresented = true
E RootScreen: Failed to present paywall after 5 attempts - SDK bug: isPaywallPresented stuck at true
Code Sample
// AndroidManifest.xml
<activity
android:name="com.superwall.sdk.paywall.view.SuperwallPaywallActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
// Presenting the paywall
val handler = PaywallPresentationHandler()
handler.onPresent { paywallInfo ->
Log.d(TAG, "onPresent: Paywall presented - ${paywallInfo.identifier}")
}
handler.onDismiss { paywallInfo, result ->
Log.d(TAG, "onDismiss: Paywall dismissed - result: $result")
}
handler.onSkip { reason ->
Log.d(TAG, "onSkip: Paywall skipped - reason: $reason")
}
handler.onError { error ->
Log.e(TAG, "onError: Paywall error - $error")
}
Superwall.instance.register(
placement = "onboarding_completed",
handler = handler,
feature = {
Log.d(TAG, "feature block called")
}
)Workarounds Attempted (None Worked)
- Added
launchMode="singleTop"to SuperwallPaywallActivity - no effect - Retry with delays (500ms × 5 attempts) -
isPaywallPresentednever resets - Called
Superwall.instance.reset()- did not resetisPaywallPresented - Upgraded from 2.6.5 to 2.6.8 - bug persists
Related Issues
- Superwall-Flutter #23: Faulty state management when moving app to background - Similar state management issue
Changelog References
These fixes in recent versions suggest related issues have been addressed before:
- v2.6.7: "Fix handling of deep links when paywall is detached"
- v2.6.5: "Fixes composable paywall state updates not firing in onAttach"
- v2.5.7: "Fixes paywall navigation resetting after backgrounding"
- v2.5.6: "Fix potential issue with paywall not dismissing due to paywall_decline concurrency issue"
Impact
This bug completely breaks the paywall flow for users who tap external links. The only recovery is to force-quit and restart the app, which is a very poor user experience.
Suggested Fix
- The SDK should properly handle the activity lifecycle when Chrome Custom Tabs is opened, preserving the paywall state
- If the paywall activity is destroyed, it should be restored when the user returns
- If the paywall IS dismissed for any reason,
isPaywallPresentedmust be reset tofalse