Propagate default package across workflow steps#6790
Merged
Conversation
2 tasks
Each workflow step now gets its own cached PackageContext. Package-bearing steps use their own package selection; packageless steps fall back to the workflow's singleStepFallbackId context. Sheet dismissal restores the step-local selection rather than resetting to the workflow-global fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add WorkflowContext.effectivePackageContext(for:) which returns the step's own package context when it has package components, falling back to the global workflowPackageContext otherwise. WorkflowPaywallView now stores this per-page and sets it as the workflowPackageContext env value, so TabsComponentView and any other consumer automatically gets the step-scoped default rather than the global workflow fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the P1 fix, workflowPackageContext in the env is the step-local stable WorkflowPackageContext struct. selectedPackageContextOverride.package is a @published var that mutates on user selection, so using it as defaultPackage caused planSelectionDefaultPackage to track the user's current selection rather than the page's configured default. Removing the override prefix restores correct, stable defaultPackage derivation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
effectivePackageContext already falls back to workflowPackageContext internally, so the trailing ?? workflowPackageContext was unreachable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When navigating forward from step N to step N+1, step N+1 now opens with step N's current selection if that package is available there. Falls back to the workflow-global default (singleStepFallbackId), then the step's own authored default as last resort. Backward navigation is unchanged: the cached PackageContext for the previous step is returned as-is, so selection does not propagate back. Revisiting a step via forward navigation always re-derives from the current step's live selection (not a stale first-visit cache). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Once a step's PackageContext has been initialized (first visit), subsequent forward navigations to that step return the cached context unchanged. This matches Android's setDefaultPackage idempotency: a step that has already been visited keeps its selection — whether that came from a prior carry-forward or from the user tapping a package on that step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ontext carry-forward - Make `buildPackageContext` internal so tests can call it directly - `testPackageContextMutationsPropagateThroughStepCacheReference`: catches any future refactoring of PackageContext from class to struct, which would silently break back-navigation selection preservation - `testBuildPackageContextCarriesForwardPreferredPackageWhenAvailableInStep`: end-to-end forward carry-forward at the WorkflowPaywallView layer - `testBuildPackageContextReturnsEmptyContextForPackagelessStepWithNoFallback`: guards the nil-effectiveContext → empty PackageContext path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents that `WorkflowPaywallView.renderedPageForStep` uses the cache-hit path (ignoring `carryForwardPackage`) when a step already has a `PackageContext` in `stepPackageContexts`, preserving the user's own prior selection over a new carry-forward from the previous step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xt(for:preferring:) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… test `testPackageContextMutationsPropagateThroughStepCacheReference` only tested Swift reference semantics in isolation. The same invariant is already exercised meaningfully in `testCachedStepContextTakesPrecedenceOverCarryForwardOnRevisit`, which now carries a note pointing to the exact mutation line. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rce step's In multi-offering workflows where two steps expose the same package identifier from different offerings, effectivePackageContext(for:preferring:) was returning the source step's Package instance directly. Switch both the preferredPackage path and the wfDefault path from contains+direct-return to first(where:) so the selectedPackage always comes from base.packages (the destination step's list). Tests: two new WorkflowContextTests cases covering each path with distinct offerings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
42637bc to
2f81a5c
Compare
Extract sheet-dismissal package restoration into `RootView.restoredPackageAfterSheetDismissal` (static, testable) and add `RootViewSheetDismissalTests` covering workflow-context restore, nil-snapshot fallback, non-workflow-context always-default, and nil-default edge case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vegaro
reviewed
May 14, 2026
vegaro
reviewed
May 14, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The property was set in init but never read — the init body at line 149 already uses the parameter directly (still in scope), so self.selectedPackageContextOverride was unreachable dead weight holding an unnecessary strong reference. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vegaro
reviewed
May 18, 2026
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 94b61ac. Configure here.
vegaro
approved these changes
May 21, 2026
vegaro
left a comment
Member
There was a problem hiding this comment.
It would be good to test it still works on sheets and tabs but I think this is good to go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Supersedes and closes #6735.
Checklist
purchases-androidand hybridsMotivation
Multi-step workflow paywalls had no way to carry the user's package selection forward. If a user picked Annual on step 1 and navigated to step 2, that step would fall back to the workflow's
singleStepFallbackIddefault or its own page default. The same issue applied on back navigation — returning to a previous step and going forward again re-applied a stale default.Android counterpart: RevenueCat/purchases-android#3431
Description
Per-step
PackageContextcacheWorkflowPaywallViewnow maintains a[String: PackageContext]dictionary keyed by step ID. The first time a step is rendered itsPackageContextis built and stored; subsequent renders (e.g. back + forward) reuse the cached instance, so any in-flight user selection is preserved across navigation.Step-scoped
workflowPackageContextenvironmentEach page now injects its own
effectiveWorkflowPackageContext(the per-stepWorkflowPackageContext) into the environment rather than broadcasting the globalsingleStepFallbackIdcontext for every step. This meansTabsComponentViewand any other package-initialisation code reads the correct step-local default, not the workflow fallback.WorkflowContext.effectivePackageContext(for:)— new helper that returns the step's own package context when the step has packages, falling back to the globalworkflowPackageContextfor packageless steps.selectedPackageContextOverrideonPaywallsV2ViewAn optional override allows
WorkflowPaywallViewto inject the pre-built (and potentially already-mutated by the user)PackageContextdirectly instead of havingPaywallsV2Viewconstruct a fresh one on every render.Analytics
defaultPackagefixplanSelectionDefaultPackagenow derives from the stable, configured default rather than the mutable current selection. The code inLoadedPaywallsV2View.bodyis unchanged — it still readsself.workflowPackageContext?.selectedPackage— but the value it sees is now the step-localeffectiveWorkflowPackageContextinjected per-step rather than the global workflow context broadcast to every step. SinceWorkflowPackageContext.selectedPackageis immutable, this is always the step's configured default, not the user's in-flight selection.Sheet dismissal fix
RootViewsnapshots the package before a selection sheet opens; when the sheet is dismissed without a new choice in a workflow context, it restores the snapshot rather than falling back to the global workflow default.Testing
WorkflowContextTests—packageContext(for:)andeffectivePackageContext(for:): selected-by-default, first-package fallback, packageless step (nil), missing step (nil), step-local beats global fallback, packageless step falls back to global, no packages anywhere (nil).WorkflowPaywallViewTests—packageContext(for:)viaWorkflowContextfor a step with own packages, a packageless step, and a missing step.Note
Medium Risk
Updates workflow navigation and package-selection state in Paywalls V2, so regressions could affect which package is preselected/purchased across steps, tabs, and sheets.
Overview
Enables package selection carry-forward across multi-step Paywalls V2 workflows by caching a per-step
PackageContextinWorkflowPaywallView, injecting a step-scopedworkflowPackageContext, and lettingPaywallsV2Viewaccept aselectedPackageContextOverrideso cached selections are reused rather than rebuilt.WorkflowContextnow exposespackageContext(for:)andeffectivePackageContext(for:preferring:)to resolve step-local packages with fallback to the workflow’ssingleStepFallbackIdpackage context, andTabsComponentView/RootViewwere adjusted to avoid overwriting cached selections (tab initialization prefers the parent’s cached package; sheet dismissal restores the pre-sheet snapshot in workflow contexts).Adds focused unit tests for the new workflow back-navigation destination API, package-context resolution/carry-forward behavior, tab revisit inheritance, and sheet dismissal restoration.
Reviewed by Cursor Bugbot for commit 0d39f25. Bugbot is set up for automated code reviews on this repo. Configure here.