Skip to content

Propagate default package across workflow steps#6790

Merged
facumenzella merged 27 commits into
mainfrom
worktree-majestic-mapping-dijkstra
May 25, 2026
Merged

Propagate default package across workflow steps#6790
facumenzella merged 27 commits into
mainfrom
worktree-majestic-mapping-dijkstra

Conversation

@facumenzella

@facumenzella facumenzella commented May 14, 2026

Copy link
Copy Markdown
Member

Supersedes and closes #6735.

Checklist

  • Unit tests
  • Follow-up issues for purchases-android and hybrids

Motivation

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 singleStepFallbackId default 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 PackageContext cache

WorkflowPaywallView now maintains a [String: PackageContext] dictionary keyed by step ID. The first time a step is rendered its PackageContext is 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 workflowPackageContext environment

Each page now injects its own effectiveWorkflowPackageContext (the per-step WorkflowPackageContext) into the environment rather than broadcasting the global singleStepFallbackId context for every step. This means TabsComponentView and 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 global workflowPackageContext for packageless steps.

selectedPackageContextOverride on PaywallsV2View

An optional override allows WorkflowPaywallView to inject the pre-built (and potentially already-mutated by the user) PackageContext directly instead of having PaywallsV2View construct a fresh one on every render.

Analytics defaultPackage fix

planSelectionDefaultPackage now derives from the stable, configured default rather than the mutable current selection. The code in LoadedPaywallsV2View.body is unchanged — it still reads self.workflowPackageContext?.selectedPackage — but the value it sees is now the step-local effectiveWorkflowPackageContext injected per-step rather than the global workflow context broadcast to every step. Since WorkflowPackageContext.selectedPackage is immutable, this is always the step's configured default, not the user's in-flight selection.

Sheet dismissal fix

RootView snapshots 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

WorkflowContextTestspackageContext(for:) and effectivePackageContext(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).

WorkflowPaywallViewTestspackageContext(for:) via WorkflowContext for 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 PackageContext in WorkflowPaywallView, injecting a step-scoped workflowPackageContext, and letting PaywallsV2View accept a selectedPackageContextOverride so cached selections are reused rather than rebuilt.

WorkflowContext now exposes packageContext(for:) and effectivePackageContext(for:preferring:) to resolve step-local packages with fallback to the workflow’s singleStepFallbackId package context, and TabsComponentView/RootView were 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.

facumenzella and others added 11 commits May 14, 2026 07:42
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>
@facumenzella facumenzella force-pushed the worktree-majestic-mapping-dijkstra branch from 42637bc to 2f81a5c Compare May 14, 2026 13:44
@facumenzella facumenzella marked this pull request as ready for review May 14, 2026 13:51
@facumenzella facumenzella requested review from a team as code owners May 14, 2026 13:51
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>
Comment thread RevenueCatUI/Templates/V2/Components/Root/RootView.swift Outdated
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift Outdated
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift
Comment thread RevenueCatUI/Purchasing/WorkflowContext.swift
facumenzella and others added 2 commits May 14, 2026 10:17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift
Comment thread RevenueCatUI/Templates/V2/Components/Tabs/TabsComponentView.swift
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift Outdated
Comment thread RevenueCatUI/Templates/V2/PaywallsV2View.swift Outdated
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>
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift Outdated
Comment thread RevenueCatUI/Templates/V2/WorkflowPaywallView.swift Outdated
@facumenzella facumenzella requested a review from vegaro May 18, 2026 17:45

@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 1 potential issue.

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 94b61ac. Configure here.

Comment thread RevenueCatUI/Purchasing/WorkflowContext.swift Outdated

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

one question on behavior

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

It would be good to test it still works on sheets and tabs but I think this is good to go

@facumenzella facumenzella merged commit af6b9cf into main May 25, 2026
18 of 20 checks passed
@facumenzella facumenzella deleted the worktree-majestic-mapping-dijkstra branch May 25, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants