State-driven Paywalls: Tab selected-state#7019
Conversation
4 builds increased size
RevenueCat 1.0 (1)
|
| 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 |
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 |
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 |
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 |
🛸 Powered by Emerge Tools
Comment trigger: Size diff threshold of 100.00kB exceeded
📸 Snapshot Test1 modified, 338 unchanged
🛸 Powered by Emerge Tools |
c8a29f2 to
6922dfc
Compare
6922dfc to
99a44a5
Compare
…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>
a25493c to
f5fc958
Compare
… of github.com:RevenueCat/purchases-ios into monika/paywall-state-management/pweng-64-ios-tab-state
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.





Checklist
purchases-androidand hybridsMotivation
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
stateDeclarationscondition, 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
stateUpdatesinto the store, with the selected tab id as the$valuepayload.@StateObject).ConditionContext, so astateDeclarationscondition 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
paywallStateValuesandpaywallStateDefaultswhenever the store changes (PaywallsV2Viewfor standalone,WorkflowPaywallViewfor workflows). Stack, Text, Image, and Icon views read those env values and pass them intoConditionContext, sostate-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"tostateDeclarationsonPaywallComponentsData.Reviewed by Cursor Bugbot for commit 81fef1d. Bugbot is set up for automated code reviews on this repo. Configure here.