Add workflow-based paywall resolution#6675
Conversation
- Add `getWorkflows` and `getWorkflow` endpoint paths - Add `WorkflowsListResponse` and `PublishedWorkflow` response models - Add `WorkflowDetailProcessor` to handle `inline`/`use_cdn` response actions - Add `WorkflowCdnFetcher` protocol and `DirectWorkflowCdnFetcher` implementation - Add `GetWorkflowsOperation` and `GetWorkflowOperation` cacheable network operations - Add `WorkflowsAPI` facade and wire into `Backend` - Add unit tests for all new components iOS equivalent of RevenueCat/purchases-android#3300 Made-with: Cursor
- Add WorkflowTrigger struct (name, type, action_id, component_id) - Add triggers, outputs, and metadata fields to WorkflowStep - Add metadata field to PublishedWorkflow - Remove value field from WorkflowTriggerAction (backend uses step_id only) Made-with: Cursor
- Replace value/resolvedTargetStepId assertions with stepId - Remove testDecodeWorkflowTriggerActionValueTakesPrecedence (value field removed) - Add testDecodeWorkflowTrigger for new WorkflowTrigger struct - Add testDecodePublishedWorkflowWithMetadata - Update testDecodeWorkflowStepDefaults to cover triggers, outputs, metadata - Add testDecodeWorkflowStepMatchingActualBackendResponse with real backend payload Made-with: Cursor
Made-with: Cursor
…o callbacks CDN fetch and JSON decoding were running once per deduplicated callback. Compute the Result<WorkflowFetchResult, BackendError> once outside the performOnAllItemsAndRemoveFromCache loop, matching the pattern used by GetOfferingsOperation and other operations in the codebase. Made-with: Cursor
…purchases-ios into feat/workflows-network-layer
…y errors correctly - Replace Data(contentsOf:) with URLSession.shared.dataTask + DispatchSemaphore in DirectWorkflowCdnFetcher; gets URLSession timeout, HTTP status validation, and proper network stack semantics - Add WorkflowDetailProcessingError.cdnFetchFailed typed error so CDN I/O failures are distinguishable from envelope parsing failures - Catch cdnFetchFailed in GetWorkflowOperation and map to NetworkError.networkError instead of NetworkError.decoding, fixing misleading error classification - Update WorkflowDetailProcessorTests to assert the typed error is thrown Made-with: Cursor
…gContinuation
- Make WorkflowCdnFetcher.fetchCompiledWorkflowData async throws; use
withCheckedThrowingContinuation to bridge URLSession.dataTask into async,
avoiding any thread-blocking
- Make WorkflowDetailProcessor.process async throws to propagate async
- Bridge into async in GetWorkflowOperation via Task {}; completion() is
called inside the Task after CDN fetch and decoding complete
- Update WorkflowDetailProcessorTests to async throws with await
Made-with: Cursor
Avoids Task{} in GetWorkflowOperation and keeps all operations calling
completion() synchronously from within the HTTP callback, matching every
other operation in the codebase.
- WorkflowCdnFetcher.fetchCompiledWorkflowData now takes a completion handler;
DirectWorkflowCdnFetcher uses URLSession.dataTask (non-blocking, no semaphore)
- WorkflowDetailProcessor.process now takes a completion handler; inline action
completes synchronously, use_cdn fans out to the fetcher callback
- GetWorkflowOperation splits into getWorkflow/handleResponse/backendResult/
distribute helpers to stay within line-length limits
- WorkflowDetailProcessorTests updated to use waitUntilValue pattern
Made-with: Cursor
Space-separated appUserID+workflowId could collide (e.g. user 'a b' + workflow 'c' == user 'a' + workflow 'b c'). Use newline as delimiter, matching the precedent set by GetWebBillingProductsOperation. Made-with: Cursor
WorkflowResponseTests already had @_spi(Internal) @testable import RevenueCat; WorkflowDetailProcessorTests was missing the @_spi annotation, causing 'id', 'steps', and 'initialStepId' to be inaccessible on PublishedWorkflow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts targets The PBXBuildFile entries for DB7EA77F2F9764F700BCC082 and DB7EA7802F9764F700BCC082 were referenced in the StoreKitUnitTests and UnitTests source phases but never defined, causing MockWorkflowsAPI to be missing at compile time and MockBackend.swift to fail with 'cannot find MockWorkflowsAPI in scope'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android PR #3350 split offering_id (internal ID) from offering_identifier (human-readable identifier used for lookup). iOS was only decoding offering_id and using it for the offering lookup, which would fail if the workflow response sends the public identifier in offering_identifier. - Add offeringIdentifier (from offering_identifier) to WorkflowScreen - Update PurchaseHandler to prefer offeringIdentifier, falling back to offeringId - Fix typo: workflow.screelns -> workflow.screens - Add decode tests for both offering fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nt fixtures Stack component decoding requires a padding field; test JSON fixtures were missing it causing keyNotFound decode errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t fixtures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…check The -EnableWorkflowsEndpoint launch argument replaces the compile-time flag, allowing workflow resolution to be toggled without rebuilding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop WorkflowScreen.offeringId / offering_id CodingKey (Android already uses offering_identifier exclusively — aligns both platforms) - Simplify PurchaseHandler to use screen.offeringIdentifier directly - No-paywall offerings in PaywallsTester now render as tappable rows (workflow mode) instead of plain read-only text, with a context menu restricted to the Workflow option Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… extension Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 builds increased size
RevenueCat 1.0 (1)
|
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 39.5 kB |
| Code Signature | ⬆️ 3.2 kB |
| DYLD.Exports | ⬆️ 1.1 kB |
| Other | ⬆️ 64.6 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 21.4 kB (0.18%)
Total download size change: ⬆️ 6.2 kB (0.16%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.Exports | ⬆️ 3.0 kB |
| 🗑 RevenueCat.WorkflowFetchResult | ⬇️ -1.6 kB |
| RevenueCat.DefaultValue.value witness | ⬇️ -1.4 kB |
| RevenueCat.IgnoreDecodeErrors.value witness | ⬆️ 1.4 kB |
| 📝 RevenueCat.WorkflowDataResult.value witness | ⬆️ 1.2 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 46.5 kB (0.18%)
Total download size change: ⬆️ 10.5 kB (0.17%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 15.3 kB |
| DYLD.String Table | ⬆️ 6.5 kB |
| RevenueCatUI.Template2View.iconImage | ⬇️ -3.7 kB |
| RevenueCatUI.TabsComponentViewModel.init(component,controlStackVi... | ⬆️ 3.5 kB |
| DYLD.Exports | ⬆️ 3.0 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 17.2 kB (0.16%)
Total download size change: ⬆️ 7.4 kB (0.18%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.Exports | ⬆️ 3.0 kB |
| 🗑 RevenueCat.WorkflowFetchResult | ⬇️ -1.6 kB |
| RevenueCat.DefaultValue.value witness | ⬇️ -1.4 kB |
| RevenueCat.IgnoreDecodeErrors.value witness | ⬆️ 1.4 kB |
| 📝 RevenueCat.WorkflowDataResult.value witness | ⬆️ 1.2 kB |
🛸 Powered by Emerge Tools
Comment trigger: Size diff threshold of 100.00kB exceeded
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 ce7314c. Configure here.
| identifier: identifier, | ||
| presentedOfferingContext: presentedOfferingContext | ||
| ) | ||
| } |
There was a problem hiding this comment.
Workflow resolution doesn't fall back to standard offerings
High Severity
The PR description states the workflow path "falls back to standard offerings on failure," but resolveOfferingIdentifier uses return try await for the workflow branch, meaning any error from resolveWorkflowOfferingIdentifier propagates immediately without ever reaching the standard offerings code path below the #if block. When the -EnableWorkflowsEndpoint flag is active and the workflow call fails (network error, missing step/screen, etc.), the user sees an error instead of the standard offering paywall. A do/catch wrapping the workflow call is needed so failures can fall through to standard resolution.
Reviewed by Cursor Bugbot for commit ce7314c. Configure here.






Summary
Reuses the networking layer from feat/workflows-network-layer (no duplicate code) and adds paywall resolution on top.
-EnableWorkflowsEndpointlaunch argument is set,PaywallViewfetches a workflow, resolves initial step → screen → offering, maps the screen component config toOffering.PaywallComponents, and falls back to standard offerings on failureWorkflowsAPI/GetWorkflowOperationfrom the base branchWorkflowDataResult,PublishedWorkflow,WorkflowStep,WorkflowScreenare@_spi(Internal) publicsoRevenueCatUIcan access themWorkflowScreenMapperconvertsWorkflowScreen+UIConfigintoOffering.PaywallComponentsPurchases.workflow(forOfferingIdentifier:)is@_spi(Internal)and delegates tobackend.workflowsAPIOffering.withPaywallComponents(_:)helper addedofferings(),cachedOfferings, andworkflow()added toPaywallPurchasesTypesoPaywallViewConfiguration.Contentno longer callsPurchases.shareddirectly — callers inject aPaywallPurchasesTypeinstance viapurchaseHandler.purchasesInstanceasync letWorkflowScreen.offeringId/offering_idremoved — aligns with Android which already migrated tooffering_identifierexclusivelyParity with Android: RevenueCat/purchases-android#3350
Waiting for #6661
Previous PR for reference: #6640
Test plan
WorkflowScreenMapperunit tests verify field mapping toPaywallComponentsDataGetWorkflowOperationtests covered byBackendGetWorkflowsTestsWorkflowDetailProcessortests: inline unwrap, CDN fetch, hash verification, error casesPaywallViewConfigurationTests: cached offering, offering resolution per content type-EnableWorkflowsEndpointlaunch argument and verify workflow-served paywall renders correctly, including offerings with no pre-existing paywallNote
Medium Risk
Adds a new workflow-based offering resolution path that changes how
PaywallView/exit-offer prefetch fetch the offering (including parallel workflow+offerings requests) when a launch flag is enabled, which could impact paywall rendering and selection logic. Risk is mitigated by being gated behind-EnableWorkflowsEndpointand falling back to standard offerings on errors, but it touches networking models and paywall configuration flow.Overview
Adds workflow-based paywall resolution (opt-in via
-EnableWorkflowsEndpoint). When content is.offeringIdentifier,PurchaseHandlercan now fetch a workflow, follow initial step → screen → offering id, and buildOffering.PaywallComponentsfrom the workflow screen to render a workflow-served Paywalls V2 UI.Refactors paywall offering resolution to be injected and testable.
PaywallViewConfiguration.Contentno longer callsPurchases.shareddirectly; insteadPaywallPurchasesTypegainsofferings(),cachedOfferings, and (non-tvOS)workflow(forOfferingIdentifier:), withPaywallView,PaywallViewController, andView+PresentPaywallrouting throughPurchaseHandlerfor cached initial offering + async resolution.Networking/model updates for workflows. Renames
WorkflowFetchResult→WorkflowDataResult, exposes workflow types via@_spi(Internal)forRevenueCatUI, updates workflow envelope parsing, and addsPurchases.workflow(forOfferingIdentifier:)(SPI) to callWorkflowsAPI.Tests + sample app updates. Adds unit tests for workflow screen mapping and paywall resolution, expands workflow decoding coverage, and updates PaywallsTester to support a new Workflow presentation mode (including making offerings without paywalls tappable).
Reviewed by Cursor Bugbot for commit ce7314c. Bugbot is set up for automated code reviews on this repo. Configure here.