Skip to content

Inject device locale into paywall webview at document start - #503

Merged
yusuftor merged 4 commits into
developfrom
claude/paywall-translation-perf-o43go3
Aug 14, 2026
Merged

yusuftor merged 4 commits into
developfrom
claude/paywall-translation-perf-o43go3

Conversation

@ianrumac

Copy link
Copy Markdown
Contributor

Changes in this pull request

  • Paywalls with translations rendered in the default language and visibly re-rendered once template_variables arrived, since that message is gated on receipt and product loading. A WKUserScript now sets window.SW_DEVICE_PRELOAD = {"deviceLocale": ...} at document start — before any page JavaScript runs — so paywall.js can resolve localized strings on its first render. template_variables is unchanged and remains the source of truth.

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Paywalls with translations rendered in the default language and visibly
re-rendered once template_variables arrived, since that message is gated
on receipt and product loading. A WKUserScript now sets
window.__SW_DEVICE_PRELOAD__ = {"deviceLocale": ...} at document start —
before any page JavaScript runs — so paywall.js can resolve localized
strings on its first render. template_variables is unchanged and remains
the source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01775Up1AYfMgNQybxjnoDSg
@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown

PR author is not in the allowed authors list.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No correctness issues found in the Swift. Two things worth confirming before merge, both about what the change doesn't cover.

Reviewed changes — the full diff at f405d64: a new document-start user script carrying the device locale into the paywall web view.

  • DevicePreloadScript — new enum namespace whose source(deviceLocale:) builds window.__SW_DEVICE_PRELOAD__ = {"deviceLocale":"…"};, using JSONSerialization so a host-supplied locale override can't break out of the statement.
  • SWWebView injection — the script is registered as a .atDocumentStart, main-frame-only WKUserScript on wkConfig.userContentController, alongside the existing paywallMessageHandler registration.
  • Factory widening — SWWebView.init's factory becomes FeatureFlagsFactory & DeviceHelperFactory; every call site already passes DependencyContainer, which conforms to both, so no call-site churn.
  • Tests + changelog — three Swift Testing cases over the string builder, and an Unreleased changelog entry.

I verified the one thing that looked risky: adding a user script to wkConfig.userContentController after super.init(frame:configuration:) does reach the live web view. WKWebViewConfiguration's copy shares the underlying content controller by reference (WebKit's APIPageConfiguration holds it in a LazyInitializedRef that super.init force-materializes on the caller's object before copying), and init never loads a URL — loadURL(from:) runs later. The pre-existing message-handler registration on the same object is production proof of the same mechanism.

ℹ️ The paywall.js half of the contract can't be verified from this repo

The global name window.__SW_DEVICE_PRELOAD__ and the key deviceLocale are a contract with the web bundle, and nothing in the SDK pins it. If paywall.js reads a different global or key, this ships as a silent no-op that nobody notices — there's no log, no assertion, and the paywall still renders correctly via template_variables. Linking the paywall.js counterpart in the PR description would settle it.

Technical details
# Web-side contract for `window.__SW_DEVICE_PRELOAD__` is unpinned

## Affected sites
- `Sources/SuperwallKit/Paywall/View Controller/Web View/DevicePreloadScript.swift:19-27` — global name and payload key are string literals with no counterpart in this repo.

## Required outcome
- Confirm `paywall.js` reads `window.__SW_DEVICE_PRELOAD__.deviceLocale` under exactly that name, and that the paywall.js change is released before or alongside this SDK version.

## Open questions for the human
- Does `paywall.js` resolve translations from `deviceLocale`, or from `preferredLocale`? The two differ when the host app is not localized for the user's preferred language — `deviceLocale` is `Locale.autoupdatingCurrent.identifier` (the locale the app itself resolves to), which is why `preferredLocale` / `preferredLanguageCode` were added in the first place (`CHANGELOG.md:1129`). Preloading `deviceLocale` is consistent with how the SDK already keys paywall localization server-side (`Network.swift:97`, `Endpoint.swift:174-190`, `ConfigLogic.getStaticPaywall`), so this is likely correct — but if the page resolves translations from `preferredLocale`, the first paint will still land in the wrong language and the flicker this PR targets remains.

ℹ️ The injection itself is untested — only the string builder is

DevicePreloadScriptTests covers source(deviceLocale:) as a pure function, which is the part least likely to regress. The part that carries the behavior — that a script with .atDocumentStart and the right locale actually lands on the web view's content controller — has no coverage, so a future refactor could drop or reorder the registration silently. Several suites already build an SWWebView from a DependencyContainer, so asserting on webView.configuration.userContentController.userScripts is cheap.

Technical details
# No test covers the `WKUserScript` registration

## Affected sites
- `Sources/SuperwallKit/Paywall/View Controller/Web View/SWWebView.swift:119-133` — registration path with no test.
- `Tests/SuperwallKitTests/Paywall/View Controller/Web View/DevicePreloadScriptTests.swift` — covers only the string builder.

## Required outcome
- A test that constructs an `SWWebView` and asserts exactly one user script whose `source` matches `DevicePreloadScript.source(deviceLocale: dependencyContainer.makeDeviceInfo().locale)`, with `injectionTime == .atDocumentStart` and `isForMainFrameOnly == true`.

## Suggested approach
- Follow the existing construction pattern in `Tests/SuperwallKitTests/Paywall/View Controller/SurveyManagerTests.swift:20-33` (real `DependencyContainer`, real `PaywallMessageHandler`); the assertion reads off `webView.configuration.userContentController.userScripts`.

ℹ️ Nitpicks

  • CHANGELOG.md:5 — ## Unreleased is the first use of that heading in this file; every other section is a released version number, and CLAUDE.md describes adding the entry as part of the version bump. Worth confirming the release flow handles the placeholder heading rather than expecting a version.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using Claude Opus | 𝕏

yusuftor and others added 3 commits August 14, 2026 13:46
4.16.2 hasn't shipped yet, so the entry rides the pending release
section instead of a separate Unreleased header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JErpaZs7R3kJ4HHHA1P4nm
…I input

zh_Hans_CN is pure ASCII, so the case never tested what its name
claimed, and its contains assertion was already implied by the
exact-equality test above. Feed a genuinely non-ASCII locale and pin
the full output, documenting that JSONSerialization emits it as raw
UTF-8 rather than \u-escaped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JErpaZs7R3kJ4HHHA1P4nm

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the single test-only commit since the prior review at f405d64; the production Swift surface is untouched.

  • Non-ASCII test now exercises non-ASCII — handlesNonAsciiLocales swaps the ASCII input zh_Hans_CN for 中文_CN and replaces the contains check with exact string equality, so it pins the full emitted statement rather than a substring. This addresses the prior review's inline thread on that case.

I confirmed the new expectation is correct rather than aspirational: running the same builder logic against Foundation, JSONSerialization.data(withJSONObject:) emits non-ASCII as raw UTF-8 with no \u escaping and no whitespace, so {"deviceLocale":"中文_CN"} is exactly what the function produces. The assertion would now fail if the encoder ever started escaping or the wrapper text drifted.

Pullfrog  | View workflow run | Using Claude Opus | 𝕏

@yusuftor
yusuftor merged commit 4dfa44e into develop Aug 14, 2026
4 checks passed
@yusuftor
yusuftor deleted the claude/paywall-translation-perf-o43go3 branch August 14, 2026 12:52
@pullfrog pullfrog Bot mentioned this pull request Aug 14, 2026
8 of 10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants