Skip to content

State-driven Paywalls: Tab selected-state#7019

Merged
MonikaMateska merged 14 commits into
mainfrom
monika/paywall-state-management/pweng-64-ios-tab-state
Jun 17, 2026
Merged

State-driven Paywalls: Tab selected-state#7019
MonikaMateska merged 14 commits into
mainfrom
monika/paywall-state-management/pweng-64-ios-tab-state

Conversation

@MonikaMateska

@MonikaMateska MonikaMateska commented Jun 16, 2026

Copy link
Copy Markdown
Member

Checklist

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

Motivation

This is the first interactive component in the State-driven Paywalls project, and the end-to-end proof of the Phase 0 foundation. Phase 0 landed the plumbing, the session state store, the stateDeclarations condition, and the resolver hook, but nothing wrote to the store or reacted to it yet. This makes a Tab's selection actually drive other components on the same paywall.

Resolves: PWENG-64

Description

  • Write on selection (and initial appearance), the Tabs component dispatches its stateUpdates into the store, with the selected tab id as the $value payload.
  • Broadcast both presentation roots republish the store's current values/defaults as environment values whenever it changes (the root owns the store as a @StateObject).
  • Read override-resolving components read that snapshot into ConditionContext, so a stateDeclarations condition re-resolves when the selected tab changes. Reading the env value is also what subscribes the view, so only the components that read state re-render, not the whole tree.

Scope: the read path is wired for Stack, Text, Image and Icon (the common reactors). Button, Package, Timeline, Carousel and Video aren't threaded yet, the resolver already supports them.

Reactivity note: I went with the "redraw readers via environment value" approach rather than making the store an @EnvironmentObject, because the store is optional (workflow vs. standalone vs. previews), and value-keyed env dependencies give precise, crash-free invalidation. Granularity is per-snapshot for now (any state change re-resolves all readers); per-key subscription is an easy future optimization if it's ever needed.

State-driven tabs example in PaywallsTester:

Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-06-16.at.13.50.59.mov

Note

Medium Risk
Touches paywall override resolution and SwiftUI invalidation across many V2 components; behavior is covered by new tests but Button/Package and other component types are not wired yet.

Overview
Tabs now write selected-tab id into the paywall state store on first appear and on every tab change via stateUpdates + $value, so sibling components can react without waiting on package logic.

Presentation roots republish paywallStateValues and paywallStateDefaults whenever the store changes (PaywallsV2View for standalone, WorkflowPaywallView for workflows). Stack, Text, Image, and Icon views read those env values and pass them into ConditionContext, so state-conditioned overrides re-resolve when the tab changes; only views that read the snapshot invalidate.

Also adds TabsComponentStateTests, a PaywallsTester “State-driven tabs” demo paywall, and renames JSON decoding from "state" to stateDeclarations on PaywallComponentsData.

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

@emerge-tools

emerge-tools Bot commented Jun 16, 2026

Copy link
Copy Markdown

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 18.7 MB ⬆️ 36.8 kB (0.2%) 67.8 MB ⬆️ 155.2 kB (0.23%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.local-source
1.0 (1) 4.3 MB ⬆️ 4.9 kB (0.11%) 13.0 MB ⬆️ 12.9 kB (0.1%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.cocoapods
1.0 (1) 6.5 MB ⬆️ 8.5 kB (0.13%) 28.7 MB ⬆️ 37.9 kB (0.13%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.spm
1.0 (1) 4.4 MB ⬆️ 5.3 kB (0.12%) 11.3 MB ⬆️ 12.6 kB (0.11%) N/A

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 155.2 kB (0.23%)
Total download size change: ⬆️ 36.8 kB (0.2%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 70.9 kB
Code Signature ⬆️ 3.8 kB
DYLD.Exports ⬆️ 2.2 kB
Strings.Unmapped ⬆️ 1.0 kB
Other ⬆️ 77.2 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: ⬆️ 12.9 kB (0.1%)
Total download size change: ⬆️ 4.9 kB (0.11%)

Largest size changes

Item Install Size Change
📝 RevenueCatUI.ImageComponentViewModel.styles(state,condition,isEli... ⬆️ 3.9 kB
🗑 RevenueCatUI.ImageComponentViewModel.styles(state,condition,isEli... ⬇️ -3.9 kB
🗑 RevenueCatUI.StackComponentViewModel.styles(state,condition,isEli... ⬇️ -3.2 kB
📝 RevenueCatUI.StackComponentViewModel.styles(state,condition,isEli... ⬆️ 3.2 kB
🗑 RevenueCatUI.TextComponentViewModel.styles(state,condition,select... ⬇️ -2.9 kB
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: ⬆️ 37.9 kB (0.13%)
Total download size change: ⬆️ 8.5 kB (0.13%)

Largest size changes

Item Install Size Change
DYLD.String Table ⬆️ 14.2 kB
📝 RevenueCatUI.ImageComponentViewModel.styles(state,condition,isEli... ⬆️ 3.9 kB
🗑 RevenueCatUI.ImageComponentViewModel.styles(state,condition,isEli... ⬇️ -3.9 kB
🗑 RevenueCatUI.StackComponentViewModel.styles(state,condition,isEli... ⬇️ -3.2 kB
📝 RevenueCatUI.StackComponentViewModel.styles(state,condition,isEli... ⬆️ 3.2 kB
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: ⬆️ 12.6 kB (0.11%)
Total download size change: ⬆️ 5.3 kB (0.12%)

Largest size changes

Item Install Size Change
📝 RevenueCatUI.ImageComponentViewModel.styles(state,condition,isEli... ⬆️ 3.9 kB
🗑 RevenueCatUI.ImageComponentViewModel.styles(state,condition,isEli... ⬇️ -3.9 kB
🗑 RevenueCatUI.StackComponentViewModel.styles(state,condition,isEli... ⬇️ -3.2 kB
📝 RevenueCatUI.StackComponentViewModel.styles(state,condition,isEli... ⬆️ 3.2 kB
🗑 RevenueCatUI.TextComponentViewModel.styles(state,condition,select... ⬇️ -2.9 kB
View Treemap

Image of diff


🛸 Powered by Emerge Tools

Comment trigger: Size diff threshold of 100.00kB exceeded

@MonikaMateska MonikaMateska added the pr:feat A new feature label Jun 16, 2026
@emerge-tools

emerge-tools Bot commented Jun 16, 2026

Copy link
Copy Markdown

📸 Snapshot Test

1 modified, 338 unchanged

Name Added Removed Modified Renamed Unchanged Errored Approval
RevenueCat
com.revenuecat.PaywallsTester
0 0 1 0 273 0 ⏳ Needs approval
PaywallsTester V1 swift-snapshot-testing
com.revenuecat.PaywallsTester.v1-snapshots
0 0 0 0 65 0 N/A

🛸 Powered by Emerge Tools

@MonikaMateska MonikaMateska force-pushed the pw-state/5-resolver branch 6 times, most recently from c8a29f2 to 6922dfc Compare June 16, 2026 21:27
Base automatically changed from pw-state/5-resolver to main June 17, 2026 08:32
facumenzella and others added 5 commits June 17, 2026 10:47
…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 MonikaMateska force-pushed the monika/paywall-state-management/pweng-64-ios-tab-state branch from a25493c to f5fc958 Compare June 17, 2026 08:47
@MonikaMateska MonikaMateska marked this pull request as ready for review June 17, 2026 08:47
@MonikaMateska MonikaMateska requested review from a team as code owners June 17, 2026 08:47
Comment thread RevenueCatUI/Templates/V2/PaywallsV2View.swift Outdated
Comment thread RevenueCatUI/Templates/V2/Components/Tabs/TabsComponentView.swift
… of github.com:RevenueCat/purchases-ios into monika/paywall-state-management/pweng-64-ios-tab-state

@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 846f6cd. Configure here.

Comment thread RevenueCatUI/Templates/V2/PaywallsV2View.swift Outdated
@MonikaMateska MonikaMateska merged commit 29781b0 into main Jun 17, 2026
41 of 43 checks passed
@MonikaMateska MonikaMateska deleted the monika/paywall-state-management/pweng-64-ios-tab-state branch June 17, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants