From 0408122991d22bcd30ef38850c15dc2dc9f10b11 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 12:24:07 +0000 Subject: [PATCH 1/5] Inject device locale into paywall WebView at document start Translated paywalls rendered in the default language first, then visibly re-rendered once the template_variables message delivered deviceLocale to paywall.js (that message is gated on product/billing loading, so it can take seconds). The web runtime now reads window.__SW_DEVICE_PRELOAD__ at boot and seeds its locale from it, so inject that global before any page JavaScript runs: - Add DevicePreloadScript, a pure builder that serializes the payload with kotlinx.serialization so hostile locale strings cannot break out of the script, producing exactly: window.__SW_DEVICE_PRELOAD__ = {"deviceLocale":"en_US"}; - Install it via WebViewCompat.addDocumentStartJavaScript (androidx.webkit, new dependency) when the WebView supports DOCUMENT_START_SCRIPT, and fall back to evaluateJavascript in WebViewClient.onPageStarted on older WebView versions. - The locale comes from PaywallViewState.locale, which is the same DeviceHelper.locale value later sent as deviceLocale in template_variables, so the later message is a visual no-op. - Unit-test the builder (exact output, quote escaping, longer and non-ASCII locales) and add a CHANGELOG entry. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg --- CHANGELOG.md | 3 + gradle/libs.versions.toml | 2 + superwall/build.gradle.kts | 3 + .../view/webview/DefaultWebviewClient.kt | 2 + .../view/webview/DevicePreloadScript.kt | 32 +++++++ .../sdk/paywall/view/webview/SWWebView.kt | 49 ++++++++++ .../view/webview/WebviewFallbackClient.kt | 3 +- .../view/webview/DevicePreloadScriptTest.kt | 96 +++++++++++++++++++ 8 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt create mode 100644 superwall/src/test/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScriptTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index cb7a5980c..c7f0a3a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw - Custom purchases produce full transaction analytics (`transaction_start`/`transaction_complete`, `subscriptionStart`/`freeTrialStart`) with an SDK-generated transaction identifier exposed as `StoreProduct.customTransactionId`, and free-trial eligibility for custom products is derived from the customer's entitlement history. - Adds `ApiStoreProduct`, a product backed by Superwall API data, used for custom store products. +## Fixes +- Paywalls with translations now render in the user's language on first paint instead of briefly showing the default language. The device locale is injected into the paywall webview before any page JavaScript runs (via a document-start script, with an `onPageStarted` fallback on older WebView versions), so the paywall no longer waits for product loading to learn the locale. + ## Breaking Changes - System back presses are now forwarded into the paywall as a `back_button_input` message instead of dismissing it directly: multi-page flows navigate back one page, and paywalls with nowhere to go back (root page, single page) close themselves through the standard manual-close path (`Declined`/`ManualClose`) — so single-page paywalls dismiss the same as before, from the app's perspective. When `reroute_back_button` is enabled in Paywall settings, the `PaywallOptions.onBackPressed` app callback keeps first refusal before the press is forwarded. Paywalls built on runtimes that predate `back_button_input` will ignore the press; this means that previously existing Multi-page paywalls, if republished, will now navigate back inside the paywall once republished. - Removes the deprecated `SuperwallBillingFlowParams.Builder.setSkuDetails(SkuDetails)`. Billing Library 9 removes `SkuDetails` entirely, so this method can no longer exist. Use `setProductDetailsParamsList(...)` with `ProductDetails` instead. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9856090fc..75632a43c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] billing_version = "9.1.0" browser_version = "1.8.0" +webkit_version = "1.12.1" gradle_plugin_version = "8.6.1" jna_version = "5.14.0@aar" kotlinxCoroutinesGuavaVersion = "1.9.0" @@ -61,6 +62,7 @@ revenue_cat = { module = "com.revenuecat.purchases:purchases", version.ref = "re # Browser browser = { module = "androidx.browser:browser", version.ref = "browser_version" } +webkit = { module = "androidx.webkit:webkit", version.ref = "webkit_version" } # Compose compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_version" } diff --git a/superwall/build.gradle.kts b/superwall/build.gradle.kts index 7561d19d4..454d8cc18 100644 --- a/superwall/build.gradle.kts +++ b/superwall/build.gradle.kts @@ -169,6 +169,9 @@ dependencies { // Browser implementation(libs.browser) + // WebView (document-start script injection) + implementation(libs.webkit) + // Core implementation(libs.core) implementation(libs.appcompat) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt index f5a166339..d460dd44e 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DefaultWebviewClient.kt @@ -20,6 +20,7 @@ internal open class DefaultWebviewClient( private val ioScope: CoroutineScope, private val onWebViewCrash: (view: WebView, RenderProcessGoneDetail) -> Unit = { v, d -> }, private val localResourceHandler: LocalResourceHandler? = null, + private val onPageStartedHook: (WebView) -> Unit = {}, ) : WebViewClient() { val webviewClientEvents: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 10, replay = 2) @@ -45,6 +46,7 @@ internal open class DefaultWebviewClient( favicon: Bitmap?, ) { super.onPageStarted(view, url, favicon) + view?.let(onPageStartedHook) } override fun onPageFinished( diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt new file mode 100644 index 000000000..08fb7c800 --- /dev/null +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt @@ -0,0 +1,32 @@ +package com.superwall.sdk.paywall.view.webview + +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +/** + * Builds the JavaScript snippet that seeds the paywall web runtime with device + * data before any page JavaScript runs. + * + * The web runtime reads `window.__SW_DEVICE_PRELOAD__` at boot and uses + * `deviceLocale` to render translations on first paint, instead of waiting for + * the `template_variables` message (which is gated on product/billing loading). + * The locale value must be identical to the `deviceLocale` the SDK later sends + * in `template_variables`, so that message is a visual no-op. + */ +internal object DevicePreloadScript { + /** + * Returns a one-line script of the form: + * `window.__SW_DEVICE_PRELOAD__ = {"deviceLocale":"en_US"};` + * + * The payload is serialized with kotlinx.serialization so hostile locale + * strings (quotes, backslashes, etc.) are escaped and cannot break out of + * the JSON literal. + */ + fun build(deviceLocale: String): String { + val payload = + buildJsonObject { + put("deviceLocale", deviceLocale) + } + return "window.__SW_DEVICE_PRELOAD__ = $payload;" + } +} 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 cfcfb0c0f..b23075554 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 @@ -25,6 +25,8 @@ import android.webkit.WebView import android.webkit.WebViewClient import android.widget.EditText import androidx.core.graphics.createBitmap +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent @@ -179,8 +181,53 @@ class SWWebView( private var lastWebViewClient: WebViewClient? = null private var lastLoadedUrl: String? = null + // The device preload script seeds `window.__SW_DEVICE_PRELOAD__` before any + // page JavaScript runs, so translated paywalls render in the device locale on + // first paint instead of waiting for the `template_variables` message. + private var devicePreloadScript: String? = null + private var documentStartScriptInstalled = false + + private fun currentDeviceLocale(): String? = + delegate?.state?.locale + ?: if (Superwall.initialized) { + Superwall.instance.dependencyContainer.deviceHelper.locale + } else { + null + } + + private fun installDevicePreloadScript() { + val locale = currentDeviceLocale() ?: return + val script = DevicePreloadScript.build(locale) + devicePreloadScript = script + if (documentStartScriptInstalled) { + return + } + try { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*")) + documentStartScriptInstalled = true + } + } catch (e: Throwable) { + // Fall back to injecting in onPageStarted via the webview client. + Logger.debug( + LogLevel.warn, + LogScope.paywallView, + "Failed to install document-start device preload script: ${e.message}", + ) + } + } + + // Fallback for WebView versions without document-start script support: + // inject as early as possible once the page starts loading. + private val onPageStartedPreloadHook: (WebView) -> Unit = { view -> + if (!documentStartScriptInstalled) { + devicePreloadScript?.let { view.evaluateJavascript(it, null) } + } + } + internal fun prepareWebview() { addJavascriptInterface(messageHandler, "SWAndroid") + installDevicePreloadScript() val webSettings = this.settings setWebContentsDebuggingEnabled(false) @@ -235,6 +282,7 @@ class SWWebView( } }, localResourceHandler = localResourceHandler, + onPageStartedHook = onPageStartedPreloadHook, ) this.webViewClient = client if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { @@ -300,6 +348,7 @@ class SWWebView( } }, localResourceHandler = localResourceHandler, + onPageStartedHook = onPageStartedPreloadHook, ) this.webViewClient = client diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt index 15585db81..2f08a7015 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/WebviewFallbackClient.kt @@ -27,7 +27,8 @@ internal class WebviewFallbackClient( private val stopLoading: () -> Unit, private val onCrashed: (view: WebView, RenderProcessGoneDetail) -> Unit, localResourceHandler: LocalResourceHandler? = null, -) : DefaultWebviewClient("", ioScope, onCrashed, localResourceHandler) { + onPageStartedHook: (WebView) -> Unit = {}, +) : DefaultWebviewClient("", ioScope, onCrashed, localResourceHandler, onPageStartedHook) { private class MaxAttemptsReachedException : Exception("Max attempts reached") private var failureCount = 0 diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScriptTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScriptTest.kt new file mode 100644 index 000000000..3ef928e97 --- /dev/null +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScriptTest.kt @@ -0,0 +1,96 @@ +package com.superwall.sdk.paywall.view.webview + +import com.superwall.sdk.Given +import com.superwall.sdk.Then +import com.superwall.sdk.When +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.junit.Assert.assertEquals +import org.junit.Test + +class DevicePreloadScriptTest { + private fun payloadOf(script: String): JsonObject { + val prefix = "window.__SW_DEVICE_PRELOAD__ = " + assertEquals(prefix, script.take(prefix.length)) + assertEquals(";", script.takeLast(1)) + val json = script.removePrefix(prefix).removeSuffix(";") + return Json.decodeFromString(JsonObject.serializer(), json) + } + + @Test + fun `builds exact preload script for a simple locale`() { + Given("a simple device locale") { + val locale = "en_US" + When("building the preload script") { + val script = DevicePreloadScript.build(locale) + Then("it matches the exact one-liner the web runtime expects") { + assertEquals( + "window.__SW_DEVICE_PRELOAD__ = {\"deviceLocale\":\"en_US\"};", + script, + ) + } + } + } + } + + @Test + fun `escapes hostile locale strings so they cannot break out of the script`() { + Given("a hostile locale string containing quotes and JS") { + val locale = "en\"};alert(1);//" + When("building the preload script") { + val script = DevicePreloadScript.build(locale) + Then("the quote is escaped inside the JSON literal") { + assertEquals( + "window.__SW_DEVICE_PRELOAD__ = {\"deviceLocale\":\"en\\\"};alert(1);//\"};", + script, + ) + } + Then("the payload round-trips back to the original value") { + assertEquals( + locale, + payloadOf(script)["deviceLocale"]!!.jsonPrimitive.content, + ) + } + } + } + } + + @Test + fun `handles longer non-ASCII locales`() { + Given("a longer locale with script and region subtags") { + val locale = "zh_Hans_CN" + When("building the preload script") { + val script = DevicePreloadScript.build(locale) + Then("it matches the exact one-liner") { + assertEquals( + "window.__SW_DEVICE_PRELOAD__ = {\"deviceLocale\":\"zh_Hans_CN\"};", + script, + ) + } + Then("the payload round-trips back to the original value") { + assertEquals( + locale, + payloadOf(script)["deviceLocale"]!!.jsonPrimitive.content, + ) + } + } + } + } + + @Test + fun `preserves non-ASCII characters`() { + Given("a locale string containing non-ASCII characters") { + val locale = "ja_JP_日本" + When("building the preload script") { + val script = DevicePreloadScript.build(locale) + Then("the payload round-trips back to the original value") { + assertEquals( + locale, + payloadOf(script)["deviceLocale"]!!.jsonPrimitive.content, + ) + } + } + } + } +} From f2c1ba76fdb89bbf7f40d760d59197c947dd00b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 10 Aug 2026 12:43:44 +0000 Subject: [PATCH 2/5] Update coverage badge [skip ci] --- .github/badges/branches.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg index 2ea735435..7a1b36d4e 100644 --- a/.github/badges/branches.svg +++ b/.github/badges/branches.svg @@ -1 +1 @@ -branches36.3% \ No newline at end of file +branches36.1% \ No newline at end of file From 897b33239a1521f2e36ec6109b2ac8f63d3d45bd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 13:50:07 +0000 Subject: [PATCH 3/5] Drop androidx.webkit; inject device preload via onPageStarted eval only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The document-start script path required adding androidx.webkit as a new dependency for every SDK user. It bought little: the paywall runtime reads window.__SW_DEVICE_PRELOAD__ when its network-fetched bundle boots, so an evaluateJavascript from onPageStarted lands well before that — and since the web runtime now seeds exclusively from the preload global, a missed injection just means today's behavior (wait for template_variables), never a wrong translation. This matches how the SDK already injects JS (plain evaluateJavascript, like the selection/zoom scripts), just hooked at page start rather than template delivery, which would be too late. DevicePreloadScript and its tests are unchanged; the script is now built lazily in the hook so it always uses the freshest locale. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg --- CHANGELOG.md | 2 +- gradle/libs.versions.toml | 2 - superwall/build.gradle.kts | 3 -- .../view/webview/DevicePreloadScript.kt | 2 +- .../sdk/paywall/view/webview/SWWebView.kt | 43 ++++--------------- 5 files changed, 10 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f0a3a4d..06fb3fc46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw - Adds `ApiStoreProduct`, a product backed by Superwall API data, used for custom store products. ## Fixes -- Paywalls with translations now render in the user's language on first paint instead of briefly showing the default language. The device locale is injected into the paywall webview before any page JavaScript runs (via a document-start script, with an `onPageStarted` fallback on older WebView versions), so the paywall no longer waits for product loading to learn the locale. +- Paywalls with translations now render in the user's language on first paint instead of briefly showing the default language. The device locale is injected into the paywall webview as soon as the page starts loading, so the paywall no longer waits for product loading to learn the locale. ## Breaking Changes - System back presses are now forwarded into the paywall as a `back_button_input` message instead of dismissing it directly: multi-page flows navigate back one page, and paywalls with nowhere to go back (root page, single page) close themselves through the standard manual-close path (`Declined`/`ManualClose`) — so single-page paywalls dismiss the same as before, from the app's perspective. When `reroute_back_button` is enabled in Paywall settings, the `PaywallOptions.onBackPressed` app callback keeps first refusal before the press is forwarded. Paywalls built on runtimes that predate `back_button_input` will ignore the press; this means that previously existing Multi-page paywalls, if republished, will now navigate back inside the paywall once republished. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75632a43c..9856090fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] billing_version = "9.1.0" browser_version = "1.8.0" -webkit_version = "1.12.1" gradle_plugin_version = "8.6.1" jna_version = "5.14.0@aar" kotlinxCoroutinesGuavaVersion = "1.9.0" @@ -62,7 +61,6 @@ revenue_cat = { module = "com.revenuecat.purchases:purchases", version.ref = "re # Browser browser = { module = "androidx.browser:browser", version.ref = "browser_version" } -webkit = { module = "androidx.webkit:webkit", version.ref = "webkit_version" } # Compose compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_version" } diff --git a/superwall/build.gradle.kts b/superwall/build.gradle.kts index 454d8cc18..7561d19d4 100644 --- a/superwall/build.gradle.kts +++ b/superwall/build.gradle.kts @@ -169,9 +169,6 @@ dependencies { // Browser implementation(libs.browser) - // WebView (document-start script injection) - implementation(libs.webkit) - // Core implementation(libs.core) implementation(libs.appcompat) diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt index 08fb7c800..83d751f78 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/webview/DevicePreloadScript.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.json.put /** * Builds the JavaScript snippet that seeds the paywall web runtime with device - * data before any page JavaScript runs. + * data as soon as the page starts loading. * * The web runtime reads `window.__SW_DEVICE_PRELOAD__` at boot and uses * `deviceLocale` to render translations on first paint, instead of waiting for 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 b23075554..730c8151f 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 @@ -25,8 +25,6 @@ import android.webkit.WebView import android.webkit.WebViewClient import android.widget.EditText import androidx.core.graphics.createBitmap -import androidx.webkit.WebViewCompat -import androidx.webkit.WebViewFeature import com.superwall.sdk.Superwall import com.superwall.sdk.analytics.internal.track import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent @@ -181,12 +179,12 @@ class SWWebView( private var lastWebViewClient: WebViewClient? = null private var lastLoadedUrl: String? = null - // The device preload script seeds `window.__SW_DEVICE_PRELOAD__` before any - // page JavaScript runs, so translated paywalls render in the device locale on - // first paint instead of waiting for the `template_variables` message. - private var devicePreloadScript: String? = null - private var documentStartScriptInstalled = false - + // 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 + // paywall runtime reads the global when its (network-fetched) bundle boots, + // so an onPageStarted injection lands well before it; if it ever misses, the + // runtime just falls back to waiting for `template_variables` as before. private fun currentDeviceLocale(): String? = delegate?.state?.locale ?: if (Superwall.initialized) { @@ -195,39 +193,14 @@ class SWWebView( null } - private fun installDevicePreloadScript() { - val locale = currentDeviceLocale() ?: return - val script = DevicePreloadScript.build(locale) - devicePreloadScript = script - if (documentStartScriptInstalled) { - return - } - try { - if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { - WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*")) - documentStartScriptInstalled = true - } - } catch (e: Throwable) { - // Fall back to injecting in onPageStarted via the webview client. - Logger.debug( - LogLevel.warn, - LogScope.paywallView, - "Failed to install document-start device preload script: ${e.message}", - ) - } - } - - // Fallback for WebView versions without document-start script support: - // inject as early as possible once the page starts loading. private val onPageStartedPreloadHook: (WebView) -> Unit = { view -> - if (!documentStartScriptInstalled) { - devicePreloadScript?.let { view.evaluateJavascript(it, null) } + currentDeviceLocale()?.let { locale -> + view.evaluateJavascript(DevicePreloadScript.build(locale), null) } } internal fun prepareWebview() { addJavascriptInterface(messageHandler, "SWAndroid") - installDevicePreloadScript() val webSettings = this.settings setWebContentsDebuggingEnabled(false) From c4aba795d2d44a3d973a6493fa86e31735f27778 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 10 Aug 2026 14:09:34 +0000 Subject: [PATCH 4/5] Update coverage badge [skip ci] --- .github/badges/branches.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg index 7a1b36d4e..95275ca45 100644 --- a/.github/badges/branches.svg +++ b/.github/badges/branches.svg @@ -1 +1 @@ -branches36.1% \ No newline at end of file +branches36.2% \ No newline at end of file From 2de33550fb0637328b186900c4df48e0aa965d16 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 11:48:56 +0000 Subject: [PATCH 5/5] Move translation-flash fix changelog entry to Unreleased Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06fb3fc46..1f557a468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub. +## Unreleased + +## Fixes +- Paywalls with translations now render in the user's language on first paint instead of briefly showing the default language. The device locale is injected into the paywall webview as soon as the page starts loading, so the paywall no longer waits for product loading to learn the locale. + ## 2.8.0 ## Enhancements @@ -11,9 +16,6 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw - Custom purchases produce full transaction analytics (`transaction_start`/`transaction_complete`, `subscriptionStart`/`freeTrialStart`) with an SDK-generated transaction identifier exposed as `StoreProduct.customTransactionId`, and free-trial eligibility for custom products is derived from the customer's entitlement history. - Adds `ApiStoreProduct`, a product backed by Superwall API data, used for custom store products. -## Fixes -- Paywalls with translations now render in the user's language on first paint instead of briefly showing the default language. The device locale is injected into the paywall webview as soon as the page starts loading, so the paywall no longer waits for product loading to learn the locale. - ## Breaking Changes - System back presses are now forwarded into the paywall as a `back_button_input` message instead of dismissing it directly: multi-page flows navigate back one page, and paywalls with nowhere to go back (root page, single page) close themselves through the standard manual-close path (`Declined`/`ManualClose`) — so single-page paywalls dismiss the same as before, from the app's perspective. When `reroute_back_button` is enabled in Paywall settings, the `PaywallOptions.onBackPressed` app callback keeps first refusal before the press is forwarded. Paywalls built on runtimes that predate `back_button_input` will ignore the press; this means that previously existing Multi-page paywalls, if republished, will now navigate back inside the paywall once republished. - Removes the deprecated `SuperwallBillingFlowParams.Builder.setSkuDetails(SkuDetails)`. Billing Library 9 removes `SkuDetails` entirely, so this method can no longer exist. Use `setProductDetailsParamsList(...)` with `ProductDetails` instead.