SIGN IN SIGN UP

Sort placements when prewarming paywall web views (#4074)

- Sorts the placement entries by placement id when picking prewarm
target offerings, so warm order no longer depends on the order the
backend happened to serialize the map in.
- Keeps `current` first and `fallbackOfferingId` last, only the
placement entries between them move.
- Matches iOS, which already sorts placement ids in
`WebBundleURLBatcher.targetOfferingIds`.
- Affects prewarm order only: the same set of offerings is warmed, and
no public API changes.


### Checklist
- [x] If applicable, unit tests
- [ ] If applicable, create follow-up issues for `purchases-ios` and
hybrids

<details><summary>Agent description</summary>

### Motivation


[PWENG-222](https://linear.app/revenuecat/issue/PWENG-222/android-sort-placements-for-webview-prewarm).
`Offerings.prewarmTargetOfferingIds()` drained
`config.offeringIdsByPlacement.values` in map order, so Android warmed
placements in whatever order the response produced while iOS warmed them
alphabetically. Came out of the review thread on
[purchases-ios#7420](https://github.com/RevenueCat/purchases-ios/pull/7420#discussion_r3830591714),
where the same type was unordered after serialization.

### Description

-
`config.offeringIdsByPlacement.toSortedMap().values.filterNotNullTo(identifiers)`
in `PrewarmTargetOfferings.kt`. That is the whole change.

Not visible in the diff: insertion order is warm order the rest of the
way down, so nothing else needed touching. `linkedSetOf` preserves it,
`OfferingWebViewPrewarmer` collects into another `linkedSetOf`,
`PaywallAssetWarming.warmWebViewUrls` passes `urls.toList()`, and
`PaywallWebViewPrewarmer` does `queue.addLast`.

Worth noting separately: `OfferingsManagerTest` asserts against
`testOfferings.prewarmTargetOfferings()` itself rather than a hardcoded
list, so it follows the new order without an edit.

### Regression gates

`orders placements alphabetically between the current offering and the
fallback` in `PrewarmTargetOfferingsTest`: builds the placements map
with `linkedMapOf("zebra" to ..., "alpha" to ...)` so insertion order
and sorted order disagree, and asserts `current`, `alpha`, `zebra`,
`fallback`.

**Limitations:** Kotlin's natural `String` order is UTF-16 code unit
order and Swift's `sorted()` is not identical to it for every input, so
the two platforms could still diverge on non-ASCII placement ids.
Identical for the ASCII ids used in practice. I have not verified
whether the backend permits non-ASCII placement ids.

**Rejected:** asking the backend for a canonical placement order instead
of sorting here. Open question from the same iOS thread, and it is
unclear whether placements carry any meaningful order beyond
`isCurrent`. If they do not, sorting is the end state; if they do, this
should be replaced rather than extended. Left as a follow-up rather than
blocking this on a backend answer.

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Internal prewarm sequencing only; same offerings are warmed with no
public API or purchase-path changes.
> 
> **Overview**
> **Paywall web view prewarm order** on Android no longer follows
whatever order the backend serialized `offeringIdsByPlacement` in.
Placement offering ids are now collected via **`toSortedMap()`**
(alphabetical by placement key) after **`current`** and before the
placement **fallback**, aligning with iOS.
> 
> The warmed set is unchanged—only **queue order** for prewarming moves.
A unit test asserts **`current` → `alpha` → `zebra` → `fallback`** when
the raw map order would put `zebra` first.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
bf59b7c4286d21c9878d57688ecbc6e3f729b7bd. 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
8bed2af98b88ff2aeda97df7b1bdd546aa7d4be2
Parent: 69b7c5a
Committed by GitHub <noreply@github.com> on 8/25/2026, 9:57:47 AM