perf: pre-warm workflow paywall step states off-thread#3420
Conversation
6127140 to
94a6c34
Compare
ff264e9 to
3abb26a
Compare
3abb26a to
0c63590
Compare
46bf10d to
59037fa
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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 950054c8b89f5655b616d773eccedead645379e9. Configure here.
|
|
||
| private val backStack = ArrayDeque<String>() | ||
| val backStackSnapshot: List<String> get() = backStack |
There was a problem hiding this comment.
backStackSnapshot returns live mutable reference, not a snapshot
Low Severity
The backStackSnapshot property name implies it returns an immutable copy of the back stack, but it actually returns a direct reference to the internal ArrayDeque. Since ArrayDeque is a MutableList, any caller holding onto this reference will observe mutations made by subsequent triggerAction or navigateBack calls. This violates the "snapshot" semantic — a toList() call would produce an actual defensive copy.
Reviewed by Cursor Bugbot for commit 950054c8b89f5655b616d773eccedead645379e9. Configure here.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3420 +/- ##
=======================================
Coverage 79.45% 79.45%
=======================================
Files 362 362
Lines 14539 14539
Branches 1976 1976
=======================================
Hits 11552 11552
Misses 2190 2190
Partials 797 797 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Computes step states for all workflow steps off the main thread immediately after the initial step renders, populating workflowStepStateCache so any forward navigation to a never-visited step is instant instead of blocking on calculateState. The cache is otherwise lazy: without this pre-warm, states are still memoized on first visit (so back-nav stays instant), but the first forward navigation to each new step pays the calculateState cost on the main thread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…epCache The sole call site already cancels the job before calling this function, so the extra cancel inside the function was misleading about ownership. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the state was updated after each step, creating N map copies and N Compose state writes for N pre-warmed steps. A single publish at the end is equivalent since stepStates is only read by tests and never drives mid-loop UI decisions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
computeStateForStep runs on Dispatchers.Default and reads _lastLocaleList at computation time. If refreshStateIfLocaleChanged fires while the job is suspended, it updates existing cache entries but any entry added after that point retains the stale locale. Fix by calling computed.update(localeList) immediately after insertion, using the authoritative _lastLocaleList value from the Main thread. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
950054c to
a661ac3
Compare
### Motivation Multi-step workflow paywalls need the UI to know which step is active, what the neighboring steps look like, and which direction a navigation is going, all before the first recomposition after a tap. Without this, the slide animation can't start from the correct offscreen position and the outgoing step can't be cleaned up safely. ### Description Introduces the ViewModel-side infrastructure that drives animated, multi-step workflow paywalls: **`WorkflowPaywallUiState`** — a new `@Stable` data class exposed from `PaywallViewModel` as `workflowState: State<WorkflowPaywallUiState?>`. It is non-null when the active paywall is a workflow, and carries: - `currentStepId` — the step currently on screen. - `stepStates` — a map of computed `PaywallState.Loaded.Components` keyed by step id. Populated lazily as the user navigates so any visited step is cheap to revisit. (Eager pre-warm is added on top in RevenueCat#3420.) - `pendingTransition` — a `WorkflowPendingTransition` (from-step, direction, monotonic id) set atomically alongside `currentStepId` so the first recomposition after a navigation already has both surfaces and their target positions. **`NavigationDirection`** — a simple `FORWARD` / `BACKWARD` enum used by `WorkflowPendingTransition` so the UI knows which way to slide. **Step state caching** — `buildStateFromStep` checks `workflowStepStateCache` before doing the heavy `calculateState` work and stores the result on first visit, so back-navigation never recomputes. **`onTransitionComplete(transitionId: Int)`** — called by the UI when a slide animation ends. Clears `pendingTransition` for the matching id (guarded against stale callbacks), letting the outgoing step be removed from the Compose slot table. **`WorkflowNavigator` cleanup** — replaces the internal `MutableStateFlow<String>` with a plain `var` (the ViewModel already owns the single source of truth) and drops `StateFlow`-related imports. The `backStack` field is exposed directly so the new tests can assert on its contents (the class is `internal`, so visibility is naturally scoped). ### Checklist - [x] Unit tests added - [ ] If applicable, create follow-up issues for \`purchases-ios\` and hybrids <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches paywall state management and workflow navigation paths, which could affect paywall rendering and step transitions; changes are scoped and backed by new unit tests. > > **Overview** > Adds **ViewModel-driven workflow UI state** to support animated, multi-step paywall workflows. `PaywallViewModel` now exposes `workflowState` (`WorkflowPaywallUiState`) plus `onTransitionComplete(transitionId)` so the UI can render the current step, cache visited step `PaywallState.Loaded.Components`, and coordinate slide transitions via a monotonic transition id and `NavigationDirection`. > > Refactors workflow navigation to be less reactive (removes `StateFlow` from `WorkflowNavigator`), updates locale-refresh to also update cached step states, and adds unit tests covering forward/back navigation, transition cleanup, and step-state caching; mocks/previews are updated to implement the new interface members. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit eb4093f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
**This is an automatic release.** ## RevenueCat SDK ### ✨ New Features * Add optional support for setting obfuscated account id to product changes (RevenueCat#3428) via Mark Villacampa (@MarkVillacampa) ## RevenueCatUI SDK ### Paywallv2 #### ✨ New Features * Add slide transition to workflow paywalls (RevenueCat#3418) via Cesar de la Vega (@vegaro) * Workflow state & ViewModel infrastructure (RevenueCat#3416) via Cesar de la Vega (@vegaro) #### 🐞 Bugfixes * Fix paywall layout direction for RTL locale overrides (PWENG-39) (RevenueCat#3425) via Monika Mateska (@MonikaMateska) * Apply ripple shape clip on a sibling Box to avoid clipping content (RevenueCat#3395) via Toni Rico (@tonidero) ### 🔄 Other Changes * build(deps): bump fastlane-plugin-revenuecat_internal from `21e02ec` to `af7bb5c` (RevenueCat#3442) via dependabot[bot] (@dependabot[bot]) * Abstract workflow page transition animation behind sealed class (RevenueCat#3430) via Cesar de la Vega (@vegaro) * Add `single_step_fallback_id` field to `PublishedWorkflow` (RevenueCat#3436) via Cesar de la Vega (@vegaro) * build(deps): bump fastlane-plugin-revenuecat_internal from `2d11430` to `21e02ec` (RevenueCat#3429) via dependabot[bot] (@dependabot[bot]) * Generalize `PaywallComponentsScaffold` for workflow reuse (RevenueCat#3417) via Cesar de la Vega (@vegaro) * perf: pre-warm workflow paywall step states off-thread (RevenueCat#3420) via Cesar de la Vega (@vegaro) * Update baseline profiles (RevenueCat#3427) via RevenueCat Git Bot (@RCGitBot) * build(deps): bump fastlane-plugin-revenuecat_internal from `d24ab26` to `2d11430` (RevenueCat#3426) via dependabot[bot] (@dependabot[bot]) * Replace unauthenticated SDKMAN install with SHA-pinned orb command (RevenueCat#3407) via Rick (@rickvdl) * Auto load paywall in paywall tester via local.properties (RevenueCat#3405) via Cesar de la Vega (@vegaro) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: this is a version/release cut that mainly updates version strings, changelogs, and doc deployment targets with no functional logic changes beyond version identifiers. > > **Overview** > Cuts the `10.4.0` release by removing `-SNAPSHOT` across the project (core `VERSION_NAME`, `Config.frameworkVersion`, sample/test app dependency versions, and the root `.version` file). > > Updates release collateral and publishing to point at `10.4.0`, including changelogs (`CHANGELOG.md`/`CHANGELOG.latest.md`), docs redirect (`docs/index.html`), and the CircleCI `docs-deploy` S3 sync path (from `10.4.0-SNAPSHOT` to `10.4.0`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f7b3604. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->



Motivation
The lazy step-state cache from #3416 already keeps back-navigation instant (each step is memoized on first visit). But the first forward navigation to a never-visited step still pays the full
calculateStatecost on the main thread, which can drop frames on a slide animation.Description
Adds
preWarmWorkflowStepCache(...), a coroutine launched onviewModelScopeimmediately after the initial step is built. It iterates every step in the workflow, computes its state onDispatchers.Default, and stores the result inworkflowStepStateCache. Once all steps finish computing,_workflowState.stepStatesis updated in a single batch write.The job is cancelled and the cache cleared whenever a new workflow loads, so a workflow swap doesn't leak stale states or compute work.
Pure perf optimization —
buildStateFromStepalready handles a missing cache entry by computing on demand, so behavior is identical with or without the pre-warm. The only observable difference is that forward navigation to a never-visited step is no longer blocking on the main thread once the pre-warm has had a chance to finish.🤖 Generated with Claude Code
Note
Medium Risk
Adds background coroutine work and shared cache mutation in
PaywallViewModelImpl, which could introduce race conditions or extra CPU usage if cancellation/timing isn’t handled as expected. Functional behavior should remain the same, but workflow navigation state updates now occur asynchronously.Overview
Reduces jank when navigating forward in multi-step workflow paywalls by precomputing and caching each step’s
PaywallStatein the background.PaywallViewModelImplnow launches aviewModelScopejob that computes missing step states onDispatchers.Default, updates locale on computed states, and then batch-updatesworkflowState.stepStates; the job is cancelled and the cache cleared when a new workflow is loaded.Reviewed by Cursor Bugbot for commit a661ac3. Bugbot is set up for automated code reviews on this repo. Configure here.