From 5415a6f2ffc3b04965e8218961cb202956958dca Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 10 Sep 2026 13:47:37 +0200 Subject: [PATCH 1/4] Pause/play media playback on resume/pause, avoid preloaded videos playing --- CHANGELOG.md | 1 + .../superwall/superapp/test/UITestHandler.kt | 2 - .../src/androidTest/assets/media-playback.mp4 | Bin 0 -> 2863 bytes .../view/webview/MediaPlaybackScriptTest.kt | 133 ++++++++++++++++++ .../paywall/view/SuperwallPaywallActivity.kt | 2 + .../view/webview/MediaPlaybackScript.kt | 41 ++++++ .../sdk/paywall/view/webview/PaywallWebUI.kt | 4 + .../sdk/paywall/view/webview/SWWebView.kt | 50 ++++++- .../paywall/view/PaywallMessageHandlerTest.kt | 4 + .../sdk/paywall/view/PaywallViewTest.kt | 4 + .../webview/SWWebViewMediaLifecycleTest.kt | 75 ++++++++++ 11 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 superwall/src/androidTest/assets/media-playback.mp4 create mode 100644 superwall/src/androidTest/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScriptTest.kt create mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScript.kt create mode 100644 superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f770821..0dbda02d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw ## Unreleased - Fix prices not showing when product/offers are fetched from cache +- Fix video loading and playing in the background on preloaded paywalls ## 2.8.2 diff --git a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt index 323a7bb2d..69792a526 100644 --- a/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt +++ b/app/src/main/java/com/superwall/superapp/test/UITestHandler.kt @@ -47,9 +47,7 @@ object UITestHandler { "${it.id}" }.joinToString(separator = ", "), ) - Superwall.instance.setUserAttributes(mapOf("is_user_eligible_for_dd_offer" to true)) Superwall.instance.register(placement = "swtest") - Superwall.instance.setUserAttributes(mapOf("is_user_eligible_for_dd_offer" to null)) }, ), UITestInfo( diff --git a/superwall/src/androidTest/assets/media-playback.mp4 b/superwall/src/androidTest/assets/media-playback.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..e5c1ccc74734baee583e7a51379c415b5b1efb27 GIT binary patch literal 2863 zcmeHIPiP!f7=PO&HIx#owAe$Cr{!6ZIl*7FCMKFX%WOyP;XYmtiNw&l9`6$pMwX%M`pjjzwh_n z_szU8#&}D0BQFU9#`Z9hcnq;>>p@i485?>t2*Xvzm>;aV4rFN!{J_|4U$F|Kzhd2F zz`e_Uq{zILwuiC7k5nv{5PVTBrJMt$#Jn1KcduPMx8ywl5Iu~INk=7+qtZ`Q>PdG> zbqDi357q+56NvG^*?o2cZihX4Aw^EOjvwbW*sABqZJp#X*x`a`JHDixfo}t^-GXv( zHAst=w>{=$u~8?MltYW+`&VM0XZk=dQB5EECy7e72SPb#XlRhM$~8030UUb)Dm}C+ z2ah7ZQLE}zUDLVmH9JP5p2f1?!~H&#I}4%^qi%I;c(Jbme<%|~$z>e2xF{Go*t z&sW%9_xg8V-n;qxjT?kL5Q!v$q+QT+u&Ls_#fQ$prgb<`Y5Pg19P|^DM=nl9W1*Gf zD{tFHF7+LLkf(BAF1KwmDW2AX{6C)cL6#=qv5?)&j4 z`k46yx5HR+y)nh(nqe5cS(|ETQv!j7DiS82cxwK!i8`M-Hit|{+6c^rQP-C(#SKlX zO&FRn1*fZ2G(9=FwzgKK+b#4(yBfxelf+VWRp28hjFcC)lWA^?rm#(&;|4Qp+>uQ` zw3keMTAS9mXbZoaNK@<7rnQc)*LWaJS9W-^(u8(DkCHB+__bok)TO%J%6SJ~}BO*FQ(`tGmArdc^ z>ABXVx9F-SVqqlP)?yeTnmHptmt+^UO=C*SRSW-eVDiM4ZE3G4v#!xLV?iyAW#R%J z+t$u;DIVLV9k+OZyIaz+V3lC=0??i@M z$F7Gf8y}YV&JvE!Ix=(WT!G8^zPX9-*Am}vCES$)m+|%PF7x%w0@r)Ez?~c`a2a23 nw#@gZ^P@S0rTLZT@K^H#F6aAa^Ru@3{^tDj{Cv;#w>JL(dvRM1 literal 0 HcmV?d00001 diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScriptTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScriptTest.kt new file mode 100644 index 000000000..4a35d6a38 --- /dev/null +++ b/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScriptTest.kt @@ -0,0 +1,133 @@ +package com.superwall.sdk.paywall.view.webview + +import android.util.Base64 +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +/** Exercises the injected script against real HTML video elements and Chromium playback. */ +@RunWith(AndroidJUnit4::class) +class MediaPlaybackScriptTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private lateinit var webView: WebView + + @Before + fun setUp() { + val video = instrumentation.context.assets.open("media-playback.mp4").use { it.readBytes() } + val source = "data:video/mp4;base64," + Base64.encodeToString(video, Base64.NO_WRAP) + val loaded = CountDownLatch(1) + instrumentation.runOnMainSync { + webView = WebView(instrumentation.targetContext) + webView.settings.javaScriptEnabled = true + webView.settings.mediaPlaybackRequiresUserGesture = false + webView.webViewClient = + object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + loaded.countDown() + } + } + webView.loadDataWithBaseURL( + "https://localhost/", + """ + + + + + """.trimIndent(), + "text/html", + "UTF-8", + null, + ) + } + assertTrue("Page did not load", loaded.await(10, TimeUnit.SECONDS)) + await("document.getElementById('playing').readyState >= 3") + } + + @After + fun tearDown() { + instrumentation.runOnMainSync { webView.destroy() } + } + + @Test + fun pausesAndResumesOnlyPreviouslyPlayingVideo() { + evaluate("document.getElementById('playing').play()") + await("document.getElementById('playing').currentTime > 0") + evaluate(MediaPlaybackScript.build(false)) + evaluate(MediaPlaybackScript.build(false)) // Repeated lifecycle callbacks must preserve the set. + assertEquals("true", evaluate("document.getElementById('playing').paused")) + val pausedTime = evaluate("document.getElementById('playing').currentTime") + Thread.sleep(150) + assertEquals(pausedTime, evaluate("document.getElementById('playing').currentTime")) + evaluate(MediaPlaybackScript.build(true)) + await("!document.getElementById('playing').paused") + await("document.getElementById('playing').currentTime !== $pausedTime") + assertEquals("true", evaluate("document.getElementById('manual').paused")) + } + + @Test + fun blocksLateAutoplayUntilPresentation() { + evaluate(MediaPlaybackScript.build(false)) + evaluate( + """ + window.late = document.getElementById('playing').cloneNode(); + late.id = 'late'; + late.autoplay = true; + document.body.appendChild(late); + """.trimIndent(), + ) + await("window.__swMediaPlayback.suspended.has(late) && late.paused") + evaluate(MediaPlaybackScript.build(true)) + await("!late.paused && late.currentTime > 0") + } + + @Test + fun doesNotResumeRemovedOrManuallyPausedMedia() { + evaluate(MediaPlaybackScript.build(true)) + evaluate("document.getElementById('playing').play()") + await("document.getElementById('playing').currentTime > 0") + evaluate("document.getElementById('playing').pause()") + evaluate(MediaPlaybackScript.build(false)) + evaluate(MediaPlaybackScript.build(true)) + assertEquals("true", evaluate("document.getElementById('playing').paused")) + + evaluate("document.getElementById('playing').play()") + await("!document.getElementById('playing').paused") + evaluate(MediaPlaybackScript.build(false)) + evaluate("window.removed = document.getElementById('playing'); removed.remove()") + evaluate(MediaPlaybackScript.build(true)) + assertEquals("true", evaluate("removed.paused")) + assertEquals("0", evaluate("window.__swMediaPlayback.suspended.size")) + } + + private fun evaluate(script: String): String { + val done = CountDownLatch(1) + val result = AtomicReference() + instrumentation.runOnMainSync { + webView.evaluateJavascript(script) { + result.set(it) + done.countDown() + } + } + assertTrue("JavaScript callback timed out", done.await(5, TimeUnit.SECONDS)) + return result.get() + } + + private fun await(condition: String) { + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5) + while (System.nanoTime() < deadline) { + if (evaluate(condition) == "true") return + Thread.sleep(50) + } + assertEquals(condition, "true", evaluate(condition)) + } +} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt index 95c7762ad..af5f9602e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/SuperwallPaywallActivity.kt @@ -788,6 +788,7 @@ class SuperwallPaywallActivity : AppCompatActivity() { override fun onResume() { super.onResume() val paywallVc = paywallView() ?: return + paywallVc.webView.onResume() if (isBottomSheetView || isPopupView) { setTransparentBackground() } @@ -801,6 +802,7 @@ class SuperwallPaywallActivity : AppCompatActivity() { super.onPause() val paywallVc = paywallView() ?: return + paywallVc.webView.onPause() mainScope.launch { paywallVc.beforeOnDestroy(forceCleanup = isFinishing) } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScript.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScript.kt new file mode 100644 index 000000000..2310bdebd --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/MediaPlaybackScript.kt @@ -0,0 +1,41 @@ +package com.superwall.sdk.paywall.view.webview + +/** Keeps hidden paywall media paused without suspending JavaScript needed for preloading. */ +internal object MediaPlaybackScript { + fun build(allowed: Boolean): String = + """ + (() => { + const key = '__swMediaPlayback'; + if (!window[key]) { + const state = { allowed: false, suspended: new Set() }; + state.pause = media => { + if (!media.paused && !media.ended) { + state.suspended.add(media); + media.pause(); + } + }; + // Capture also catches autoplay and media inserted after the initial scan. + document.addEventListener('play', event => { + if (!state.allowed && event.target instanceof HTMLMediaElement) { + state.pause(event.target); + } + }, true); + window[key] = state; + } + const state = window[key]; + state.allowed = $allowed; + if (!state.allowed) { + document.querySelectorAll('video, audio').forEach(state.pause); + } else { + const suspended = Array.from(state.suspended); + state.suspended.clear(); + suspended.forEach(media => { + if (media.isConnected && !media.ended) { + const result = media.play(); + if (result) result.catch(() => {}); + } + }); + } + })(); + """.trimIndent() +} diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/PaywallWebUI.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/PaywallWebUI.kt index 719aa167d..6c1889eb9 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/PaywallWebUI.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/PaywallWebUI.kt @@ -14,6 +14,10 @@ interface PaywallWebUI { fun enableBackgroundRendering() + fun onPause() + + fun onResume() + fun scrollBy( x: Int, y: Int, diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt index 7760e4e58..049b2dd6c 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt @@ -181,6 +181,50 @@ class SWWebView( private var lastLoadedUrl: String? = null private var loadRetryCount = 0 + private var hostPaused = false + private var viewDestroyed = false + // View callbacks can run from the superclass constructor. + private var mediaLifecycleReady = true + + private fun updateMediaPlayback() { + if (!mediaLifecycleReady || viewDestroyed) return + val allowed = !hostPaused && isAttachedToWindow && isShown && windowVisibility == View.VISIBLE + evaluateJavascript(MediaPlaybackScript.build(allowed), null) + } + + override fun onPause() { + hostPaused = true + updateMediaPlayback() + super.onPause() + } + + override fun onResume() { + super.onResume() + hostPaused = false + updateMediaPlayback() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + // A cached view can be attached to a different host, including an embedded one. + onResume() + } + + override fun onDetachedFromWindow() { + if (!viewDestroyed) evaluateJavascript(MediaPlaybackScript.build(false), null) + super.onDetachedFromWindow() + } + + override fun onWindowVisibilityChanged(visibility: Int) { + super.onWindowVisibilityChanged(visibility) + updateMediaPlayback() + } + + override fun onVisibilityChanged(changedView: View, visibility: Int) { + super.onVisibilityChanged(changedView, visibility) + updateMediaPlayback() + } + // The device preload script seeds `window.__SW_DEVICE_PRELOAD__` as soon as // the page starts loading, so translated paywalls render in the device locale // on first paint instead of waiting for the `template_variables` message. The @@ -196,6 +240,7 @@ class SWWebView( } private val onPageStartedPreloadHook: (WebView) -> Unit = { view -> + updateMediaPlayback() currentDeviceLocale()?.let { locale -> view.evaluateJavascript(DevicePreloadScript.build(locale), null) } @@ -205,7 +250,7 @@ class SWWebView( addJavascriptInterface(messageHandler, "SWAndroid") val webSettings = this.settings - setWebContentsDebuggingEnabled(false) + setWebContentsDebuggingEnabled(true) webSettings.javaScriptEnabled = true webSettings.setSupportZoom(false) webSettings.builtInZoomControls = false @@ -459,6 +504,8 @@ class SWWebView( } is WebviewClientEvent.OnPageFinished -> { + // Reinstall after navigation in case the early injection was lost. + updateMediaPlayback() // The client records page-level failures synchronously on the // WebViewClient callback thread, so this can't miss an error // whose async OnError event hasn't been processed yet. @@ -574,6 +621,7 @@ class SWWebView( } override fun destroy() { + viewDestroyed = true onScrollChangeListener = null super.destroy() } diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt index 281f42933..00d40888a 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallMessageHandlerTest.kt @@ -380,6 +380,10 @@ class PaywallMessageHandlerTest { private inner class FakePaywallWebUI( override val messageHandler: PaywallMessageHandler, ) : PaywallWebUI { + override fun onPause() = Unit + + override fun onResume() = Unit + override var delegate: PaywallUIDelegate? = null val evaluateCalls = mutableListOf() private val view = View(context) diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt index d6b265535..125f239c5 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt @@ -517,6 +517,10 @@ class PaywallViewTest { private inner class FakePaywallWebUI( override val messageHandler: PaywallMessageHandler, ) : PaywallWebUI { + override fun onPause() = Unit + + override fun onResume() = Unit + override var delegate: PaywallUIDelegate? = null var lastScrollBy: Pair? = null var lastScrollTo: Pair? = null diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt new file mode 100644 index 000000000..1fd2c98eb --- /dev/null +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt @@ -0,0 +1,75 @@ +package com.superwall.sdk.paywall.view.webview + +import android.app.Activity +import android.os.Looper +import android.view.View +import com.superwall.sdk.config.options.PaywallOptions +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.util.ReflectionHelpers + +@RunWith(RobolectricTestRunner::class) +class SWWebViewMediaLifecycleTest { + @Test + fun `media follows host pause visibility and cached reattachment`() { + val controller = Robolectric.buildActivity(Activity::class.java).setup() + val activity = controller.get() + val webView = SWWebView(activity, mockk(relaxed = true), options = { PaywallOptions() }) + try { + // A preload must remain paused even if its host resumes. + webView.onResume() + assertMediaAllowed(webView, false) + + activity.setContentView(webView) + setWindowVisibility(webView, View.VISIBLE) + assertMediaAllowed(webView, true) + webView.onPause() + assertMediaAllowed(webView, false) + webView.onResume() + assertMediaAllowed(webView, true) + + webView.visibility = View.GONE + assertMediaAllowed(webView, false) + webView.visibility = View.VISIBLE + assertMediaAllowed(webView, true) + + setWindowVisibility(webView, View.GONE) + assertMediaAllowed(webView, false) + setWindowVisibility(webView, View.VISIBLE) + assertMediaAllowed(webView, true) + + webView.onPause() + activity.setContentView(View(activity)) + assertMediaAllowed(webView, false) + // Reusing a dismissed paywall in another host must clear the old pause state. + activity.setContentView(webView) + assertMediaAllowed(webView, true) + } finally { + controller.pause().stop().destroy() + webView.destroy() + } + } + + private fun setWindowVisibility(webView: SWWebView, visibility: Int) { + shadowOf(Looper.getMainLooper()).idle() + // Robolectric attaches the view but leaves the window GONE. Emulate WindowManager's + // visibility update before dispatching the same callback Android uses. + val attachInfo = ReflectionHelpers.getField(webView, "mAttachInfo") + ReflectionHelpers.setField(attachInfo, "mWindowVisibility", visibility) + webView.dispatchWindowVisibilityChanged(visibility) + } + + private fun assertMediaAllowed(webView: SWWebView, allowed: Boolean) { + shadowOf(Looper.getMainLooper()).idle() + assertEquals( + "attached=${webView.isAttachedToWindow}, shown=${webView.isShown}, window=${webView.windowVisibility}", + MediaPlaybackScript.build(allowed), + shadowOf(webView).lastEvaluatedJavascript, + ) + } +} From 358cddfe1ca59a497f3b9774816b54153eb8d59d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 10 Sep 2026 12:06:59 +0000 Subject: [PATCH 2/4] Update coverage badge [skip ci] --- .github/badges/branches.svg | 2 +- .github/badges/jacoco.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg index 01318ccb1..ed294c4a8 100644 --- a/.github/badges/branches.svg +++ b/.github/badges/branches.svg @@ -1 +1 @@ -branches36.5% \ No newline at end of file +branches36.6% \ No newline at end of file diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg index eb67a0eb4..924227022 100644 --- a/.github/badges/jacoco.svg +++ b/.github/badges/jacoco.svg @@ -1 +1 @@ -coverage45.7% \ No newline at end of file +coverage45.8% \ No newline at end of file From 2bb598c468d94f780dedca09352e9b9634595d6c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 10 Sep 2026 14:15:47 +0200 Subject: [PATCH 3/4] Update superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> --- .../java/com/superwall/sdk/paywall/view/webview/SWWebView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt index 049b2dd6c..02d480218 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt @@ -250,7 +250,7 @@ class SWWebView( addJavascriptInterface(messageHandler, "SWAndroid") val webSettings = this.settings - setWebContentsDebuggingEnabled(true) + setWebContentsDebuggingEnabled(false) webSettings.javaScriptEnabled = true webSettings.setSupportZoom(false) webSettings.builtInZoomControls = false From ffa1a6624cf0c69d002cb9337b2bdb8fc2129d07 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 10 Sep 2026 15:06:26 +0200 Subject: [PATCH 4/4] Address review: document constructor guard, test detach while resumed Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FY2V8fiWgvPpBhfp1Wjmc9 --- .../com/superwall/sdk/paywall/view/webview/SWWebView.kt | 4 +++- .../paywall/view/webview/SWWebViewMediaLifecycleTest.kt | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt index 02d480218..6bf9c04a8 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/SWWebView.kt @@ -183,7 +183,9 @@ class SWWebView( private var hostPaused = false private var viewDestroyed = false - // View callbacks can run from the superclass constructor. + // View callbacks can run from the superclass constructor, before Kotlin initializers. + // The JVM default (false) keeps them from evaluating JS until this is set to true, so it + // must stay a field with an initializer rather than being inlined. private var mediaLifecycleReady = true private fun updateMediaPlayback() { diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt index 1fd2c98eb..98043f7c0 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/SWWebViewMediaLifecycleTest.kt @@ -43,6 +43,13 @@ class SWWebViewMediaLifecycleTest { setWindowVisibility(webView, View.VISIBLE) assertMediaAllowed(webView, true) + // Detaching dispatches window visibility before the window state changes, so a + // resumed view must still end paused. + activity.setContentView(View(activity)) + assertMediaAllowed(webView, false) + activity.setContentView(webView) + assertMediaAllowed(webView, true) + webView.onPause() activity.setContentView(View(activity)) assertMediaAllowed(webView, false)