Warm intro eligibility cache for all offerings#6839
Conversation
Widens the existing intro eligibility warm-up to cover every product across every offering (previously only the current offering's paywall-configured packages were warmed). The warm-up is staggered: products in the current offering are warmed first, then the rest, so the current offering keeps the same responsiveness as before. Also triggers the warm-up on every successful `getOfferings` call, not just on configure/foreground/login/user-switch, and resets the tracked set on `CustomerInfo` changes so the next offerings fetch re-populates it. No public API changes. Co-authored-by: Cursor <cursoragent@cursor.com>
| fetchCurrent: fetchCurrent) { [weak self] result in | ||
| if #available(iOS 15.0, macOS 12.0, watchOS 8.0, tvOS 15.0, *), | ||
| let offerings = result.value { | ||
| self?.warmUpCaches(offerings: offerings) |
There was a problem hiding this comment.
This is calling all 3
warmUpEligibilityCache
warmUpPaywallImagesCache
warmUpPaywallFontsCacheI wonder if this is too aggressive or we should only warm the eligibility cache.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 28a7b09. Configure here.
| self.operationDispatcher.dispatchOnWorkerThread { | ||
| await cache.clearEligibilityCache() | ||
| } | ||
| } |
There was a problem hiding this comment.
Race between sync cache clear and async tracking reset
Medium Severity
trialOrIntroPriceEligibilityChecker.clearCache() runs synchronously, wiping the underlying eligibility data immediately. But cache.clearEligibilityCache() is dispatched asynchronously via Task.detached(priority: .background), so warmedEligibilityProductIdentifiers still contains the old products. If getOfferings returns cached offerings before the async clear runs, warmUpEligibilityCache sees every product already in the warmed set and skips re-warming — leaving the eligibility cache empty with no prefetch until the next trigger.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 28a7b09. Configure here.
| /// Should be called whenever the underlying eligibility cache is cleared (e.g. on | ||
| /// `CustomerInfo` changes) so that the next call to ``warmUpEligibilityCache(offerings:)`` | ||
| /// re-populates the cache. | ||
| func clearEligibilityCache() { |
There was a problem hiding this comment.
Clearing out the eligibility cache only on customer info change might be too optimistic. I am afraid we could end up with stale information in the app. Should we maybe introduce a TTL duration for this cache so we have more up-to date info?
There was a problem hiding this comment.
Update: as entitlements are part of the customer info, I think the current implementation is reasonable
| /// | ||
| /// Products that have already been warmed up are skipped on subsequent calls. | ||
| /// Call ``clearEligibilityCache()`` to reset the tracking (e.g. when `CustomerInfo` changes). | ||
| func warmUpEligibilityCache(offerings: Offerings) async { |
There was a problem hiding this comment.
Definitely out of scope of this PR, but I think this strategy makes way more sense in general. We should apply this too for images.
- Load current
- Load the rest
MonikaMateska
left a comment
There was a problem hiding this comment.
Definitely something we can benefit a lot from, thanks for the implementation!


Checklist
Motivation
Apps presenting a paywall hit a cold intro/trial eligibility cache when the paywall isn't tied to the current offering's paywall-configured packages, or when
getOfferingsruns outside of configure/foreground/login. This adds a visible loading delay on prices the first time those paywalls are shown.Description
getOfferingscompletion (in addition to configure/foreground/login/user-switch).CustomerInfochanges.Notes
The prior warm-up has a blind spot: it only looks at
offering.paywall(V1) and filters bypaywall.config.packages. V2 paywall offerings (paywallComponents) are never warmed, and on V1 only packages wired intopaywall.config.packagesget warmed. Combined with the current-offering-only scope, today's warm-up covers a narrow slice.