Skip to content

Fix legacy paywalls when workflows are enabled#3598

Merged
vegaro merged 1 commit into
mainfrom
cesar/legacy-paywalls-workflows-flag
Jun 15, 2026
Merged

Fix legacy paywalls when workflows are enabled#3598
vegaro merged 1 commit into
mainfrom
cesar/legacy-paywalls-workflows-flag

Conversation

@vegaro

@vegaro vegaro commented Jun 12, 2026

Copy link
Copy Markdown
Member

Paywalls were served through the /workflows endpoint only when the offering was present in the prefetched offeringId→workflowId map. That map contains only paywalls converted to workflows, so a legacy v1 paywall errored on a 404 instead of rendering


Note

Medium Risk
Changes core paywall presentation routing when workflows are enabled; wrong branching could break legacy paywalls or workflow fetches, but scope is limited to PaywallViewModel and covered by new tests.

Overview
Fixes paywall loading when workflows are enabled but an offering is still a legacy v1 paywall (or otherwise not in the prefetch offering→workflow map). Previously everything could be forced through /workflows, which 404’d for legacy offerings.

PaywallViewModel now branches on offering.paywall == null as the durable “non-legacy / workflow” signal: with workflows on, legacy offerings (paywall present) keep the existing legacy/components path and never call /workflows; non-legacy offerings fetch via presentWorkflow, using workflowIdForOfferingId when mapped (prefetch cache key) or offering id when not yet converted.

The SDK exposes Purchases.workflowIdForOfferingId (internal API) through PurchasesType for RevenueCat UI, with mocks/tests updated to cover legacy skip, mapped workflow id, and offering-id fallback.

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

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.26%. Comparing base (935b029) to head (78142e1).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
.../com/revenuecat/purchases/PurchasesOrchestrator.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3598      +/-   ##
==========================================
- Coverage   80.26%   80.26%   -0.01%     
==========================================
  Files         378      378              
  Lines       15448    15449       +1     
  Branches     2143     2144       +1     
==========================================
  Hits        12400    12400              
- Misses       2189     2190       +1     
  Partials      859      859              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegaro vegaro force-pushed the cesar/legacy-paywalls-workflows-flag branch from 9d544af to 43cfd46 Compare June 15, 2026 09:59
@vegaro vegaro changed the title fix(paywalls): skip workflow fetch for offerings with no mapped workflow Fix legacy paywalls when workflows are enabled Jun 15, 2026
@vegaro vegaro force-pushed the cesar/legacy-paywalls-workflows-flag branch 2 times, most recently from 8956487 to 8fd591a Compare June 15, 2026 10:57
…mapped ones

When `DangerousSettings.useWorkflows` is enabled, the paywall is now served through
the /workflows endpoint for every non-legacy paywall, detected via `offering.paywall`:

- `offering.paywall != null` → legacy v1 paywall → render through the legacy path, no
  workflow fetch. This is the durable marker of a legacy paywall and survives the
  eventual removal of `paywallComponents`.
- `offering.paywall == null` → workflow paywall → fetch from /workflows, using the
  mapped workflow id when known (aligns with the prefetch cache) and otherwise the
  offering id, which the backend lazily converts for paywalls not yet migrated.

The previous gate keyed off the offeringId→workflowId map, which only contains
explicitly-converted workflows. A V2 paywall not yet converted (absent from the map)
fell through to the single-screen legacy renderer instead of being lazily converted
and rendered as a (potentially multi-page) workflow. Gating on the paywall type fixes
that while still skipping the endpoint entirely for true legacy v1 paywalls. Passing
the workflow id (not the offering id) when mapped also fixes a silent prefetch
cache-miss from the prior identifier mismatch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaro vegaro force-pushed the cesar/legacy-paywalls-workflows-flag branch from c2736c4 to 78142e1 Compare June 15, 2026 11:03
@vegaro vegaro marked this pull request as ready for review June 15, 2026 11:09
@vegaro vegaro requested a review from a team as a code owner June 15, 2026 11:09

@facumenzella facumenzella 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 solid, nice work

facumenzella added a commit to RevenueCat/purchases-ios that referenced this pull request Jun 15, 2026
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>
@vegaro vegaro added this pull request to the merge queue Jun 15, 2026
Merged via the queue into main with commit 6aa986c Jun 15, 2026
38 checks passed
@vegaro vegaro deleted the cesar/legacy-paywalls-workflows-flag branch June 15, 2026 13:13
facumenzella added a commit to RevenueCat/purchases-ios that referenced this pull request Jun 15, 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 to RevenueCat/purchases-ios 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 to RevenueCat/purchases-ios 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 to RevenueCat/purchases-ios 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