Skip to content

Warm intro eligibility cache for all offerings#6839

Merged
ajpallares merged 1 commit into
mainfrom
pallares/warm-eligibility-all-offerings
May 26, 2026
Merged

Warm intro eligibility cache for all offerings#6839
ajpallares merged 1 commit into
mainfrom
pallares/warm-eligibility-all-offerings

Conversation

@ajpallares

@ajpallares ajpallares commented May 25, 2026

Copy link
Copy Markdown
Member

Checklist

  • Unit tests added

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 getOfferings runs outside of configure/foreground/login. This adds a visible loading delay on prices the first time those paywalls are shown.

Description

  • Widens the warm-up to every product across every offering.
  • Staggers: current offering first, then the rest, preserving today's responsiveness.
  • Triggers the warm-up on getOfferings completion (in addition to configure/foreground/login/user-switch).
  • Resets the warmed product set when CustomerInfo changes.

Notes

The prior warm-up has a blind spot: it only looks at offering.paywall (V1) and filters by paywall.config.packages. V2 paywall offerings (paywallComponents) are never warmed, and on V1 only packages wired into paywall.config.packages get warmed. Combined with the current-offering-only scope, today's warm-up covers a narrow slice.

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>
@ajpallares ajpallares added the pr:fix A bug fix label May 25, 2026
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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is calling all 3

warmUpEligibilityCache
warmUpPaywallImagesCache
warmUpPaywallFontsCache

I wonder if this is too aggressive or we should only warm the eligibility cache.

@ajpallares ajpallares marked this pull request as ready for review May 26, 2026 07:48
@ajpallares ajpallares requested a review from a team as a code owner May 26, 2026 07:48

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. Load current
  2. Load the rest

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@ajpallares ajpallares requested a review from a team May 26, 2026 09:12

@MonikaMateska MonikaMateska left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely something we can benefit a lot from, thanks for the implementation!

@ajpallares ajpallares merged commit c3d362b into main May 26, 2026
41 of 43 checks passed
@ajpallares ajpallares deleted the pallares/warm-eligibility-all-offerings branch May 26, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants