Skip to content

feat(workflows): add WorkflowManager with list fetch, prefetch and offeringId resolution#6882

Merged
facumenzella merged 4 commits into
mainfrom
port/3508-workflows-manager-pr2
Jun 3, 2026
Merged

feat(workflows): add WorkflowManager with list fetch, prefetch and offeringId resolution#6882
facumenzella merged 4 commits into
mainfrom
port/3508-workflows-manager-pr2

Conversation

@facumenzella

@facumenzella facumenzella commented Jun 2, 2026

Copy link
Copy Markdown
Member

Port of RevenueCat/purchases-android#3508 (2 of 3). The cache foundation (#6881) is already merged, so this now sits directly on main.

This is the orchestration layer on top of the WorkflowsCache.

What Changed

  • New WorkflowManager (parallels OfferingsManager):
    • getWorkflow serves a fresh cached workflow straight from WorkflowsCache (no backend round-trip); on a miss it fetches, caches, and warms up the assets.
    • getWorkflowsList fetches the paywall workflows list, persists it, builds the offeringId → workflowId map, and prefetches every entry flagged prefetch == true. onComplete fires only after the list fetch and all prefetches finish (lock-guarded counter). On a backend failure it still fires, after restoring the map from disk (kept stale so the next call retries), so whoever's waiting on it is never left hanging.
    • workflowId(forOfferingId:) exposes the map.
  • Purchases.workflow(forOfferingIdentifier:) now goes through WorkflowManager: it resolves the offeringId via the map and falls back to the offering identifier itself as the workflow key. The map is empty until the list is fetched (that wiring is PR 3), so behavior is unchanged for now.
  • Wired WorkflowsCache + WorkflowManager into PurchasesFactory.

Notes

  • Android dedups in-flight requests itself; we don't need that since CallbackCache already does. I only ported the higher-level "fire once the list + prefetches are done" aggregation.
  • Asset warm-up now happens on a cache miss instead of on every workflow(forOfferingIdentifier:) call. Same net effect (assets get warmed when the cache is populated), just not redundantly re-warmed on cache hits. Flagging it so it doesn't read as an oversight.
  • Failure recovery restores the list from disk as stale (via WorkflowsCache.restoreWorkflowsListFromDisk()) rather than re-caching it as fresh, so the map keeps resolving but the next fetch still retries the backend, mirroring how the offerings cache serves its disk copy. Android currently marks it fresh, so this intentionally diverges; worth aligning Android later.

Note

Medium Risk
Touches paywall workflow loading and caching in Purchases; behavior is mostly gated until list fetch is wired, but cache/TTL and failure-recovery paths affect which workflow id and assets load.

Overview
Introduces WorkflowManager as the orchestration layer over WorkflowsCache and WorkflowsAPI, parallel to how offerings use OfferingsManager.

getWorkflow returns fresh in-memory workflow data without a network call; on miss/stale it fetches, caches, and warms paywall assets only on that path (no longer on every Purchases.workflow call). getWorkflowsList loads the paywall workflows list, persists it, builds offeringId → workflowId, and prefetches entries with prefetch == true, with onComplete after list + all prefetches (or immediately if list cache is fresh). On list fetch failure it logs a new paywall string, restores the map from disk as stale via WorkflowsCache.restoreWorkflowsListFromDisk(), and still calls onComplete.

Purchases now owns shared WorkflowsCache + WorkflowManager; workflow(forOfferingIdentifier:) resolves workflow id from the map when present, else uses the offering id, and delegates to WorkflowManager. List fetch wiring to offerings is left for a follow-up PR, so runtime behavior is unchanged until then.

Tests: WorkflowManagerTests, PurchasesWorkflowTests, and richer MockWorkflowsAPI; Xcode project entries for new sources.

Reviewed by Cursor Bugbot for commit 23ed6c5. Bugbot is set up for automated code reviews on this repo. Configure here.

@emerge-tools

emerge-tools Bot commented Jun 2, 2026

Copy link
Copy Markdown

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 18.4 MB ⬆️ 14.1 kB (0.08%) 66.6 MB ⬆️ 55.8 kB (0.08%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.local-source
1.0 (1) 4.2 MB ⬆️ 4.5 kB (0.11%) 12.8 MB ⬆️ 18.4 kB (0.15%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.cocoapods
1.0 (1) 6.4 MB ⬆️ 5.1 kB (0.08%) 28.2 MB ⬆️ 22.2 kB (0.08%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.spm
1.0 (1) 4.3 MB ⬆️ 4.1 kB (0.09%) 11.1 MB ⬆️ 14.3 kB (0.13%) N/A

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 55.8 kB (0.08%)
Total download size change: ⬆️ 14.1 kB (0.08%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 13.1 kB
📝 RevenueCat.WorkflowManager.WorkflowManager ⬆️ 9.1 kB
Code Signature ⬆️ 936 B
DYLD.Exports ⬆️ 656 B
Other ⬆️ 32.0 kB
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 18.4 kB (0.15%)
Total download size change: ⬆️ 4.5 kB (0.11%)

Largest size changes

Item Install Size Change
📝 RevenueCat.Purchases.init(appUserID,requestFetcher,receiptFetcher... ⬆️ 4.2 kB
🗑 RevenueCat.Purchases.init(appUserID,requestFetcher,receiptFetcher... ⬇️ -4.2 kB
RevenueCat.PaywallsStrings.value witness ⬆️ 1.0 kB
📝 RevenueCat.WorkflowsCache.init(deviceCache,dateProvider) ⬆️ 948 B
📝 RevenueCat.WorkflowManager.warmUpAssets(for) ⬆️ 920 B
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 22.2 kB (0.08%)
Total download size change: ⬆️ 5.1 kB (0.08%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 5.9 kB
📝 RevenueCat.Purchases.init(appUserID,requestFetcher,receiptFetcher... ⬆️ 4.2 kB
🗑 RevenueCat.Purchases.init(appUserID,requestFetcher,receiptFetcher... ⬇️ -4.2 kB
RevenueCat.PaywallsStrings.value witness ⬆️ 1.0 kB
📝 RevenueCat.WorkflowManager.warmUpAssets(for) ⬆️ 988 B
View Treemap

Image of diff

BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm

⚖️ Compare build
📦 Install build
⏱️ Analyze build performance

Total install size change: ⬆️ 14.3 kB (0.13%)
Total download size change: ⬆️ 4.1 kB (0.09%)

Largest size changes

Item Install Size Change
📝 RevenueCat.Purchases.init(appUserID,requestFetcher,receiptFetcher... ⬆️ 4.2 kB
🗑 RevenueCat.Purchases.init(appUserID,requestFetcher,receiptFetcher... ⬇️ -4.2 kB
RevenueCat.PaywallsStrings.value witness ⬆️ 1.0 kB
📝 RevenueCat.WorkflowsCache.init(deviceCache,dateProvider) ⬆️ 948 B
📝 RevenueCat.WorkflowManager.warmUpAssets(for) ⬆️ 896 B
View Treemap

Image of diff


🛸 Powered by Emerge Tools

@facumenzella facumenzella force-pushed the port/3508-workflows-manager-pr2 branch from c0aceba to 7559851 Compare June 2, 2026 08:42
@facumenzella facumenzella force-pushed the port/3508-workflows-cache-pr1 branch 2 times, most recently from 7c5ff4b to 51a9c85 Compare June 2, 2026 08:45
@facumenzella facumenzella force-pushed the port/3508-workflows-manager-pr2 branch from 7559851 to bfae745 Compare June 2, 2026 08:48
@facumenzella facumenzella force-pushed the port/3508-workflows-manager-pr2 branch 2 times, most recently from e5d1332 to 08dcac9 Compare June 2, 2026 10:11
@facumenzella facumenzella marked this pull request as ready for review June 2, 2026 13:56
@facumenzella facumenzella requested a review from a team as a code owner June 2, 2026 13:56
Comment thread Sources/Purchasing/WorkflowManager.swift
Base automatically changed from port/3508-workflows-cache-pr1 to main June 2, 2026 16:08
@facumenzella facumenzella force-pushed the port/3508-workflows-manager-pr2 branch from ea183d1 to ee563d5 Compare June 2, 2026 16:18
facumenzella and others added 2 commits June 2, 2026 18:20
…feringId resolution

Second of three stacked PRs porting purchases-android#3508 to iOS. Builds on the
WorkflowsCache foundation.

- Add `WorkflowManager` (orchestration layer paralleling `OfferingsManager`):
  - `getWorkflow` serves a fresh cached workflow without a backend round-trip,
    otherwise fetches, caches the result, and warms up its assets.
  - `getWorkflowsList` fetches the paywall workflows list, persists it, builds the
    offeringId to workflowId map, and prefetches every entry flagged prefetch=true.
    `onComplete` fires only after the list fetch AND all prefetch fetches finish
    (lock-guarded counter), and still fires on backend failure (restoring the map
    from disk first), so callers are never blocked.
  - `workflowId(forOfferingId:)` exposes the map.
- Route `Purchases.workflow(forOfferingIdentifier:)` through `WorkflowManager`,
  resolving offeringId to workflowId via the map and falling back to the offering
  identifier itself (the map is empty until the list is fetched, so behavior is
  unchanged for now). Asset warm-up now lives in `WorkflowManager`.
- Wire `WorkflowsCache` + `WorkflowManager` into `PurchasesFactory`.

Note: iOS already deduplicates in-flight requests via `CallbackCache`, so Android's
network-level request dedup is intentionally not ported; only the higher-level
"complete after list + prefetches" aggregation is. Asset warm-up now happens on a
cache miss rather than on every call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The getWorkflowsList failure path logged the error and fired onComplete()
without rehydrating the in-memory offeringId -> workflowId map, so
workflowId(forOfferingId:) returned nil after a backend failure even when a
valid previously-fetched list was still on disk. Restore it from
cachedWorkflowsListResponseFromDisk() before completing, matching the
documented WorkflowsCache contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella force-pushed the port/3508-workflows-manager-pr2 branch from ee563d5 to 1f1480e Compare June 2, 2026 16:20
Comment thread Sources/Purchasing/WorkflowManager.swift Outdated
Addresses Bugbot: the previous restore went through cache(workflowsList:),
which re-wrote the data to disk and stamped lastUpdated to now(), marking the
restored list fresh and suppressing the next backend fetch for the full cache
TTL. Add WorkflowsCache.restoreWorkflowsListFromDisk(), which rehydrates the
in-memory offeringId -> workflowId map but keeps the entry stale (and leaves
the on-disk copy untouched), so workflowId(forOfferingId:) keeps resolving the
last known list while the next call still retries the backend. Mirrors the
offerings cache disk fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@vegaro vegaro 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.

biggest thing would be paralelizing, but that can come in a future PR I think

return
}

self.backend.workflowsAPI.getWorkflow(appUserID: appUserID,

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.

so one thing I've been working on in RevenueCat/purchases-android#3508 (comment) is to paralelize these calls. Can be done in a future PR, but I think it's a good idea. Right now they're serial becauase of how backend works.

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.

Agreed it's worth doing. Keeping it out of this PR since it needs a backend-queue change rather than a tweak here, the whole SDK shares one serial OperationQueue (maxConcurrentOperationCount = 1) and GetWorkflowOperation holds the slot through the CDN download, so the fetches serialize. Will tackle it in a follow-up PR, ideally landing it the same way as the Android side.

Comment thread Sources/Purchasing/WorkflowManager.swift Outdated
Comment thread Sources/Purchasing/WorkflowManager.swift Outdated
…tch without offeringId)

- Rename WorkflowManager.workflowId(forOfferingId:) to cachedWorkflowId(forOfferingId:)
  so it's clear the value comes from the cache.
- Skip prefetching workflows with a nil offeringId, since they can't be resolved via
  cachedWorkflowId(forOfferingId:) anyway.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella enabled auto-merge (squash) June 3, 2026 10:43

@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 23ed6c5. Configure here.

Comment thread Sources/Purchasing/WorkflowManager.swift
@facumenzella facumenzella merged commit 2ceec93 into main Jun 3, 2026
18 of 20 checks passed
@facumenzella facumenzella deleted the port/3508-workflows-manager-pr2 branch June 3, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants