Skip to content

Paywall auto-dismisses when returning from external links (Chrome Custom Tabs), isPaywallPresented stays stuck at true #346

@roniabusayeed

Description

@roniabusayeed

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:

  1. 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.

  2. Secondary bug: When the paywall is dismissed (incorrectly), isPaywallPresented stays true instead of resetting to false. This causes subsequent register() calls to fail with error: SWPresentationError: 102, Paywall Already Presented.

Steps to Reproduce

  1. Configure Superwall with a paywall that has external links (Terms/Privacy)
  2. Call Superwall.instance.register(placement = "your_placement") to present the paywall
  3. Paywall presents successfully
  4. Tap on an external link in the paywall (e.g., "Terms of Service")
  5. Chrome Custom Tabs opens and displays the external page
  6. Press the back button or close Chrome Custom Tabs to return to the app
  7. Observe: Paywall is gone (auto-dismissed unexpectedly)
  8. Try to trigger the paywall again via register()
  9. Observe: Paywall does not present, error callback fires

Expected Behavior

  1. After returning from Chrome Custom Tabs, the paywall should still be visible
  2. User should be able to dismiss the paywall manually
  3. If paywall IS dismissed, isPaywallPresented should reset to false
  4. Subsequent register() calls should work normally

Actual Behavior

  1. Paywall auto-dismisses when returning from Chrome Custom Tabs
  2. isPaywallPresented remains true indefinitely
  3. Subsequent register() calls fail with "Paywall Already Presented" error
  4. 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)

  1. Added launchMode="singleTop" to SuperwallPaywallActivity - no effect
  2. Retry with delays (500ms × 5 attempts) - isPaywallPresented never resets
  3. Called Superwall.instance.reset() - did not reset isPaywallPresented
  4. Upgraded from 2.6.5 to 2.6.8 - bug persists

Related Issues

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

  1. The SDK should properly handle the activity lifecycle when Chrome Custom Tabs is opened, preserving the paywall state
  2. If the paywall activity is destroyed, it should be restored when the user returns
  3. If the paywall IS dismissed for any reason, isPaywallPresented must be reset to false

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions