SIGN IN SIGN UP

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