feat(paywalls): warm the web_views of the current offering and every placement's (#3938)
- Warms the assets of every offering a customer could be served next: the current one, each placement's, and the placement fallback. - Only `offerings.current` was warmed before, so a paywall reached through a placement was always cold. - One target set now drives image prewarming, `web_view` warming and the WebView preboot trigger, so the three cannot disagree. - The workflows topic gets the same treatment: the workflow behind every target offering is loaded and its images, `web_view` bundles and fonts warmed, one job each so a failure cannot cancel its siblings. - A decode failure for one offering never aborts the others, and nothing is attempted when no `PaywallAssetWarmer` is registered. ### Checklist - [x] If applicable, unit tests - [ ] If applicable, create follow-up issues for `purchases-ios` and hybrids <details> <summary>Agent description</summary> ### Motivation A project using placements gets one current offering per placement, so the paywall a customer actually sees is often not `offerings.current`. Warming only the current offering left every placement-reached paywall to load cold. ### Description - `prewarmTargetOfferings()`: current, every non-null placement value, and the placement fallback, deduped and resolved against `Offerings.all`. All of it arrives in the same offerings response, so it costs no extra network. Resolved once per fetch and shared by every consumer. - Resolved against `all` rather than `getCurrentOfferingForPlacement`, which allocates a presented-context copy warming has no use for. A placement mapped to null contributes nothing, matching its "show nothing here" meaning. - `PaywallAssetWarmer` gains `warmWebViewUrls`. Core hands over urls and the UI module owns the WebView rules. - `collectAssets` now collects `web_view` urls rather than a per-component asset. Nothing read that type's componentId or sizing axes: warming keys on url, and the display path builds its identity from the component style. Two components sharing a url therefore collapse to one warm, which is what the http cache does anyway. - Warming is announced off the workflow metadata map, never the byte-warm map, so a body still finishing a low-priority prefetch is not skipped. - Drive-by: the video predownloader moves onto the same `baseComponentsConfig()` helper, which also gets the offering identifier into its error log. ### Which path carries the component tree `paywall_components` is present in the offerings response only while remote config is disabled. With it enabled the tree arrives on the workflows topic instead. Both paths warm, and `OfferingWebViewPrewarmer` is a no-op in the second. ### Regression gates `getOfferings prewarms web_view bundles` catches the wiring being narrowed back to the current offering. `warms the offering each placement resolves to` and `warms the placement fallback offering` cover the target set; `ignores a placement mapped to no offering` and `ignores a placement naming an offering that is not in the response` cover its edges. `skips an offering whose component tree fails to decode, and warms the rest` covers the decode failure path, and `warms a url shared by several offerings only once` covers the dedup. </details> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Expands background warming (images, WebView engine, HTTP cache) across more offerings and workflows, with extra main-thread posting and component-tree decode. Failures are best-effort and should not change offerings delivery, but WebView/cache behavior is more aggressive. > > **Overview** > Paywall asset warming now covers every offering a customer could see next—`current`, each placement, and the placement fallback—instead of only `offerings.current`. Placement-reached paywalls no longer start cold. > > The same target set drives image pre-download, `web_view` bundle warming, and WebView engine preboot. `PaywallAssetWarmer` gains `warmWebViewUrls`; core collects URLs only (deduped), and the UI module loads them offscreen. Current-offering images still warm before `getOfferings` delivers; extra targets and bundle loads are enqueued so they do not block the backend response thread. > > Workflows get the same expansion: `WorkflowsConfigProvider` announces every target offering’s workflow (not just current) so images, fonts, and web_view URLs warm independently. Decode failures skip that offering and leave siblings unaffected. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f8f9fba81cb3853c4b77d78049fd05fbb55c9756. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Jacob Rakidzich <Jacob@JacobZivanDesign.com>
Á
Álvaro Brey committed
57f5b180372c7905998ac13626cea9d2d177c576
Parent: 395c188
Committed by GitHub <noreply@github.com>
on 8/24/2026, 1:14:43 PM