Skip to content

fix(paywalls): skip workflow fetch for offerings with no mapped workflow#7002

Merged
facumenzella merged 5 commits into
mainfrom
port/android-3598
Jun 15, 2026
Merged

fix(paywalls): skip workflow fetch for offerings with no mapped workflow#7002
facumenzella merged 5 commits into
mainfrom
port/android-3598

Conversation

@facumenzella

@facumenzella facumenzella commented Jun 15, 2026

Copy link
Copy Markdown
Member

Checklist

  • If applicable, unit tests
  • If applicable, create follow-up issues for purchases-android and hybrids

Motivation

With workflows enabled, presenting a paywall for an offering that only has a legacy paywall (no workflow on the backend) threw the user into an error state instead of just rendering the legacy paywall.

Port of RevenueCat/purchases-android#3598

Description

Route an offering to the workflows endpoint only when offering.paywall == nil (the durable marker of a non-legacy paywall). A legacy offering (paywall != nil) renders its own paywall directly, no workflow fetch. Same gate is applied to the synchronous cache seed so it stays consistent with the async path.

AI session context

AI Context

Metadata

Goal

Port purchases-android #3598 to iOS: when workflows are enabled and an offering only has a legacy paywall, render it directly instead of firing a workflow fetch that 404s into an error state.

Initial Prompt

"We need to port RevenueCat/purchases-android#3598 to iOS. Do it, then run /codex-review-fix-loop."

Key Implementation Decisions

  • Decision: gate on offering.paywall == nil (route to workflows endpoint only for non-legacy offerings).
    • Rationale: offering.paywall is the durable, intrinsic marker of a legacy v1 paywall; it doesn't depend on the timing of the workflows-list fetch. Matches the final merged Android Prepare next version: 4.33.0-SNAPSHOT #3598.
    • Rejected: gating on the offeringId → workflowId map presence (an earlier Android iteration, ported first). An offering not yet migrated to workflows is missing from the map, so a real workflow offering could be wrongly treated as legacy on a cold/stale cache. Switched after @vegaro's review pointed at the .paywall approach.
  • Decision: keep Purchases.workflow(forOfferingIdentifier:)'s map-or-?? offeringID resolution.
    • Rationale: matches Android's workflowIdForOfferingId(id) ?: id; the backend lazily converts an offering id into a workflow for paywalls not yet migrated.
  • Decision: apply the same paywall == nil guard to the synchronous cache seed (cachedInitialWorkflowContext).
    • Rationale: keep the sync seed consistent with the async gate so a legacy offering never briefly seeds a workflow paywall.

Files / Symbols Touched

  • RevenueCatUI/Purchasing/PurchaseHandler.swiftresolvePaywallViewData gate (offering.paywall == nil), unified per-content resolution, cachedInitialWorkflowContext guard.
  • RevenueCatUI/Purchasing/PaywallPurchasesType.swift, Sources/Purchasing/Purchases/Purchases.swift, RevenueCatUI/Purchasing/MockPurchases.swift, RevenueCatUI/Views/LoadingPaywallView.swift — removed the interim cachedWorkflowId(forOfferingIdentifier:) surface.
  • RevenueCatUI/Data/Strings.swift — removed the interim warning string.
  • Tests/RevenueCatUITests/Data/PaywallViewConfigurationTests.swift — workflow tests use paywall: nil trigger offerings; new legacy-render + sync-seed-guard tests.

API Impact

  • No consumer-facing public API change. No new @_spi(Internal) surface (the interim cachedWorkflowId addition was removed during the rework).

Validation

  • Commands run:
    • swift build: build complete.
    • xcodebuild test over PaywallViewConfigurationTests (18), PurchaseHandlerTests, WorkflowPaywallViewTests, PaywallViewControllerExitOfferTests: all pass, 0 failures.
    • swiftlint: no violations.
  • Branch rebased on current main. check-api-changes-revenuecatui AVFoundation drift was an unrelated main-wide issue fixed upstream by Update baseline swiftinterface files for main #7004.
  • CI: not yet green end-to-end on this revision (draft).

Review Focus

  • Confirm the iOS offering.paywall == nil semantics match Android (v1 legacy carries paywall; workflow / V2-components offerings have paywall == nil and route through the endpoint).

Risks / Reviewer Notes

  • Risk: an offering carrying both a legacy paywall and a configured workflow would render legacy (paywall wins). Matches Android's deliberate choice; paywall is the durable marker.

Omitted Context

  • Raw transcript, tool logs, and chain-of-thought were omitted.

Under workflows-enabled, presenting a paywall for an offering with only a
legacy paywall (no workflow on the backend) fired a workflow fetch that 404'd
and surfaced an error state instead of rendering the legacy paywall. Gate the
workflow path on the offeringId -> workflowId map: no mapping renders the
legacy paywall with no network call. Offerings are fetched first so the map is
populated before the decision, avoiding a cold-cache downgrade of real workflow
offerings.

Port of RevenueCat/purchases-android#3598

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella marked this pull request as ready for review June 15, 2026 12:47
@facumenzella facumenzella requested review from a team as code owners June 15, 2026 12:47
Comment thread RevenueCatUI/Purchasing/PurchaseHandler.swift Outdated

@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 2 potential issues.

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 e6a9a39. Configure here.

Comment thread RevenueCatUI/Purchasing/PurchaseHandler.swift
Comment thread RevenueCatUI/Purchasing/PurchaseHandler.swift
facumenzella and others added 2 commits June 15, 2026 15:53
Aligns with the merged purchases-android #3598: route an offering to the
workflows endpoint only when `offering.paywall == nil` (the durable marker of a
non-legacy paywall), instead of checking the offeringId -> workflowId map. The
map check was an intermediate approach the Android PR abandoned: an offering not
yet migrated to workflows is missing from the map, which would wrongly fall back
to legacy for a real workflow offering. Reading offering.paywall is authoritative
and removes the timing dependency on the map.

Drops the cachedWorkflowId plumbing (no longer needed; Purchases.workflow already
resolves the workflow id with an offering-id fallback) and the now-unused warning
string. Applies the same gate to the synchronous cache seed for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella facumenzella requested a review from vegaro June 15, 2026 14:06

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

looks great and the code is easier to follow

@facumenzella facumenzella merged commit ba9380f into main Jun 15, 2026
18 of 20 checks passed
@facumenzella facumenzella deleted the port/android-3598 branch June 15, 2026 15:06
MonikaMateska added a commit that referenced this pull request Jun 16, 2026
* fix(paywalls): skip workflow fetch for offerings with no mapped workflow (#7002)

* fix(paywalls): skip workflow fetch for offerings with no mapped workflow

Under workflows-enabled, presenting a paywall for an offering with only a
legacy paywall (no workflow on the backend) fired a workflow fetch that 404'd
and surfaced an error state instead of rendering the legacy paywall. Gate the
workflow path on the offeringId -> workflowId map: no mapping renders the
legacy paywall with no network call. Offerings are fetched first so the map is
populated before the decision, avoiding a cold-cache downgrade of real workflow
offerings.

Port of RevenueCat/purchases-android#3598

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

* Gate legacy-vs-workflow on offering.paywall, not the workflows map

Aligns with the merged purchases-android #3598: route an offering to the
workflows endpoint only when `offering.paywall == nil` (the durable marker of a
non-legacy paywall), instead of checking the offeringId -> workflowId map. The
map check was an intermediate approach the Android PR abandoned: an offering not
yet migrated to workflows is missing from the map, which would wrongly fall back
to legacy for a real workflow offering. Reading offering.paywall is authoritative
and removes the timing dependency on the map.

Drops the cachedWorkflowId plumbing (no longer needed; Purchases.workflow already
resolves the workflow id with an offering-id fallback) and the now-unused warning
string. Applies the same gate to the synchronous cache seed for consistency.

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

---------

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

* Rename PaywallComponentsData.state → stateDeclarations

---------

Co-authored-by: Facundo Menzella <facumenzella@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MonikaMateska pushed a commit that referenced this pull request Jun 17, 2026
…low (#7002)

* fix(paywalls): skip workflow fetch for offerings with no mapped workflow

Under workflows-enabled, presenting a paywall for an offering with only a
legacy paywall (no workflow on the backend) fired a workflow fetch that 404'd
and surfaced an error state instead of rendering the legacy paywall. Gate the
workflow path on the offeringId -> workflowId map: no mapping renders the
legacy paywall with no network call. Offerings are fetched first so the map is
populated before the decision, avoiding a cold-cache downgrade of real workflow
offerings.

Port of RevenueCat/purchases-android#3598

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

* Gate legacy-vs-workflow on offering.paywall, not the workflows map

Aligns with the merged purchases-android #3598: route an offering to the
workflows endpoint only when `offering.paywall == nil` (the durable marker of a
non-legacy paywall), instead of checking the offeringId -> workflowId map. The
map check was an intermediate approach the Android PR abandoned: an offering not
yet migrated to workflows is missing from the map, which would wrongly fall back
to legacy for a real workflow offering. Reading offering.paywall is authoritative
and removes the timing dependency on the map.

Drops the cachedWorkflowId plumbing (no longer needed; Purchases.workflow already
resolves the workflow id with an offering-id fallback) and the now-unused warning
string. Applies the same gate to the synchronous cache seed for consistency.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MonikaMateska added a commit that referenced this pull request Jun 17, 2026
* feat(RevenueCatUI): evaluate state conditions in the override resolver

* fix(paywalls): skip workflow fetch for offerings with no mapped workflow (#7002)

* fix(paywalls): skip workflow fetch for offerings with no mapped workflow

Under workflows-enabled, presenting a paywall for an offering with only a
legacy paywall (no workflow on the backend) fired a workflow fetch that 404'd
and surfaced an error state instead of rendering the legacy paywall. Gate the
workflow path on the offeringId -> workflowId map: no mapping renders the
legacy paywall with no network call. Offerings are fetched first so the map is
populated before the decision, avoiding a cold-cache downgrade of real workflow
offerings.

Port of RevenueCat/purchases-android#3598

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

* Gate legacy-vs-workflow on offering.paywall, not the workflows map

Aligns with the merged purchases-android #3598: route an offering to the
workflows endpoint only when `offering.paywall == nil` (the durable marker of a
non-legacy paywall), instead of checking the offeringId -> workflowId map. The
map check was an intermediate approach the Android PR abandoned: an offering not
yet migrated to workflows is missing from the map, which would wrongly fall back
to legacy for a real workflow offering. Reading offering.paywall is authoritative
and removes the timing dependency on the map.

Drops the cachedWorkflowId plumbing (no longer needed; Purchases.workflow already
resolves the workflow id with an offering-id fallback) and the now-unused warning
string. Applies the same gate to the synchronous cache seed for consistency.

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

---------

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

* feat(RevenueCatUI): broadcast paywall state snapshot via environment values

* feat(RevenueCatUI): re-resolve component overrides from paywall state

* feat(RevenueCatUI): publish Tabs selected state into the store

* test(RevenueCatUI): add PaywallsTester sample for state-driven tabs

* Use state declarations as decoding key

* fix(paywalls): seed workflow state store with page declarations

* Revert "fix(paywalls): seed workflow state store with page declarations"

This reverts commit b555709.

* fix(paywalls): stop stale state env from overriding workflow values

* Use stateDeclarations wire key in state decoding tests

* Revert Package.resolved to main

---------

Co-authored-by: Facundo Menzella <facumenzella@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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