refactor(paywalls): share one component-config lookup across asset pre-download (#4018)
Deduplicates the component walk used to gather assets for prewarming. At
this PR it's only for images and videos but the next one uses it for web
components.
Image and video asset pre-download each inlined the same block: reach
for `paywallComponents`, force the lazy `data` decode, log and bail on
failure, then take `componentsConfig.base`. Both now call one
`Offering.baseComponentsConfig()`.
### Checklist
- [x] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids
<details><summary>Agent description</summary>
### Motivation
`Offering.paywallComponents.data` is decoded lazily, and every
asset-warming caller has to handle a decode failure without propagating
it: these run before the offerings success path completes, so a
malformed component tree must not fail `getOfferings`. That obligation
was met by copy-pasting the same `getOrElse { errorLog(...); return }`
block into each pre-downloader, which meant two places to keep in sync
and two different log messages for the same condition.
Consolidating it also gives the lazy decode one documented home, which
matters because that decode is the expensive part of asset warming.
### Description
- Adds `internal fun Offering.baseComponentsConfig():
PaywallComponentsConfig?`, returning null when the offering has no
Paywalls V2 config or its component tree cannot be decoded, logging the
failure with the offering identifier.
- `OfferingImagePreDownloader.downloadV2Images` uses it. The WebView
preboot call it makes when the tree contains a `web_view` is unchanged.
- `OfferingVideoPredownloader.downloadVideos` uses it, and its early
return for an unavailable warmer becomes a guard clause instead of
wrapping the body.
- Test mocks gain an `identifier` stub, since the shared helper reads it
for the log line.
- Regression gate: the existing "component tree fails to decode" test in
each pre-downloader test class. Both assert nothing is downloaded and
nothing is thrown, which is the contract the shared helper has to keep.
</details>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Internal refactor of best-effort asset warming. Decode failures still
swallow so getOfferings is not aborted; video prefetch now walks nested
components.
>
> **Overview**
> Image and video pre-download no longer each force-decode
`paywallComponents.data` with their own `getOrElse` bail-out. Both now
use a shared `Offering.baseComponentsConfig()` that returns null (and
logs the offering id) when there is no V2 config or the lazy decode
fails.
>
> Video prefetch also walks the tree with `flatten()` instead of only
top-level stack children, so nested `VideoComponent`s get prefetched.
WebView preboot on image warming is unchanged.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c06882d6fffc9142c9e27342e596a724d9c56b2c. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY --> Á
Álvaro Brey committed
2dbe19e3ed07ef0836b90c3e2575fdd7f83f58da
Parent: b84320d
Committed by GitHub <noreply@github.com>
on 8/21/2026, 10:27:51 AM