Inject device locale into paywall WebView at document start - #450
Merged
Conversation
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes in this pull request
Bug: paywalls with translations render in the default language, then visibly re-render into the user's language once the SDK's
template_variablesmessage deliversdeviceLocaleto paywall.js. That message is gated on product/billing loading, so the default-language flash can last seconds.Fix: inject the device locale into the paywall WebView as soon as the page starts loading. The web runtime (already shipped in paywall-next) reads
window.__SW_DEVICE_PRELOAD__at boot and seeds its locale from it:evaluateJavascriptfromWebViewClient.onPageStarted— the same mechanism the SDK already uses for its selection/zoom scripts, just hooked at page start instead of template delivery (which would be too late). No new dependencies. The paywall runtime reads the global when its network-fetched bundle boots, so the page-start injection lands well before it; if it ever misses, the runtime simply waits fortemplate_variablesas it does today — the failure mode is the status quo, never a wrong translation.DefaultWebviewClient/WebviewFallbackClientgained an optionalonPageStartedHookfor this.PaywallViewState.locale(with aDeviceHelper.localefallback), which is the exact sameDeviceHelper.localesource used fordeviceLocalein the device template sent viatemplate_variables— so the later message is a visual no-op.DevicePreloadScriptobject that serializes the payload with kotlinx.serialization (buildJsonObject), never string concatenation of the raw value, so hostile locale strings (quotes, backslashes) cannot break out of the JSON literal.DevicePreloadScriptTest(JVM unit test, Given/When/Then DSL) covers the exact output foren_USandzh_Hans_CN, escaping of a hostileen"};alert(1);//string (with round-trip decode), and preservation of non-ASCII characters. Ran and passed locally:./gradlew :superwall:testDebugUnitTest --tests '*DevicePreloadScriptTest*'— 4/4 pass. The broader unit/UI test suites and demo app were not run in this environment.Counterpart changes: superwall/paywall-next branch
claude/paywall-translation-perf-o43go3(web runtime seeds its locale exclusively from__SW_DEVICE_PRELOAD__— no browser-locale fallback, since the WebView/system locale can differ from the app's resolved locale and a wrong-translation flash is worse than a source-language one) and superwall/Superwall-iOS same branch name (WKUserScriptat.atDocumentStartequivalent).Checklist
DevicePreloadScriptTestpasses locally; full suite not run in this environment.)CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.ktlintin the main directory and fixed any issues. (Code follows the repo's.editorconfigktlint_official style; no gradle ktlint task exists in this repo.)🤖 Generated with Claude Code
https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg
Generated by Claude Code