Add slide transition to workflow paywalls#3418
Conversation
f454df4 to
9e6183f
Compare
7b4fbec to
f3f173e
Compare
9e6183f to
1f8ba59
Compare
5e42321 to
dc3b6e9
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3418 +/- ##
=======================================
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:
|
1f8ba59 to
e72fe0f
Compare
46fd66d to
080ad91
Compare
e72fe0f to
b935b86
Compare
080ad91 to
8c45648
Compare
241a0f1 to
ebc6b1f
Compare
8c45648 to
553bfb7
Compare
553bfb7 to
e4e3bc9
Compare
ebc6b1f to
d71ea2c
Compare
e4e3bc9 to
fbf8377
Compare
d71ea2c to
2e368bc
Compare
Replaces the pre-rendered back-stack approach with a two-surface model: only the current step and the outgoing/incoming step are held in the slot table during a transition. All other steps are dropped. Key design: - WorkflowPaywallUiState now carries a pendingTransition (fromStepId, direction, monotonic id) set atomically with currentStepId in the ViewModel. The first recomposition after navigation already knows both surfaces and their initial positions. - key(pendingTransition.id) creates a fresh Animatable(0f) during the composition phase so Frame N's draw immediately sees the correct offscreen position — no snapTo() or withFrameNanos() needed. - LaunchedEffect only drives animateTo(1f); it no longer sets up state. - onTransitionComplete(id) is called after the animation finishes so the ViewModel can clear pendingTransition; a guard on id prevents stale callbacks from clobbering a newer transition. - Header pinning logic (hero/non-hero, backward) is simplified by removing the seenStepId gap-detection; pendingTransition covers both the "before animation starts" and "animating" cases with the same branch. - navigationDirection removed from PaywallViewModel interface; direction lives exclusively in WorkflowPendingTransition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gTransition.direction animatingDirection is NavigationDirection? but WorkflowPendingTransition.direction requires non-null. Both fields are null/non-null together (both derived from pendingTransition?.direction), so checking both in an if-expression lets the compiler smart-cast them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ontent WithOptionalBackgroundOverlay's else branch (color/image backgrounds) calls content() directly without applying its modifier parameter, so the workflowSlide translation and background were silently dropped. This caused all steps to render at position 0 (stacked on top of each other) with no background visible. Fix by wrapping in a Box that owns the fillMaxSize/workflowSlide/background modifiers, then calling WithOptionalBackgroundOverlay inside it only for the video/overlay cases that need it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ffold comment mainContent is caller-defined — for single-page paywalls it is a scrollable body, for workflow paywalls it is the slide container. Drop the stale assumption so the comment holds for both callers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3112a77 to
f63f313
Compare
| internal data class WorkflowHeaderTransitionState( | ||
| val pendingTransition: WorkflowPendingTransition?, | ||
| ) |
There was a problem hiding this comment.
Will this include more val in the near future?
facumenzella
left a comment
There was a problem hiding this comment.
I think it makes sense. Looking forward to testing again once everything is merged 👍
| val visibleStepIds: Set<String>, | ||
| val animatingFromStepId: String?, |
There was a problem hiding this comment.
This is a bit confusing to me... Could we just have a animatingFromStepId and animatingToStepId fields instead and remove the set?
Alternatively, not sure if we want the set in case we might want to support things like peeking pages in the flow... but not sure if that's something we want to do now?
**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 When the two-surface workflow slide animation (RevenueCat#3418) starts a transition, the incoming step's images may not yet be in Coil's cache, causing a visible pop-in mid-slide. Pre-warming images for every workflow step before the paywall is shown lets transitions start with warm caches. We already prewarm offerings, so we should do the same for workflows. ### Description Mirrors the iOS approach in [purchases-ios#6732](RevenueCat/purchases-ios#6732): after offerings are successfully fetched and cached, the SDK proactively fetches the workflow for the current offering and pre-downloads all of its screen images and fonts, the same pattern already used for offering paywall images via `OfferingImagePreDownloader`. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds automatic workflow fetch + asset pre-download during offerings retrieval, increasing background work and network/cache activity which could impact performance or timing-sensitive flows if misbehaving. > > **Overview** > Pre-warms workflow step assets to avoid mid-transition pop-in by **fetching the current offering’s workflow after offerings are loaded** and pre-downloading its screen images and fonts. > > This introduces `WorkflowAssetPreDownloader` (deduped by workflow id) and wires it into `WorkflowManager.getWorkflow` (pre-download failures are logged and do not block returning the workflow). `OfferingsManager` now accepts an optional `workflowPreWarmer` callback, which `PurchasesFactory` provides to trigger workflow pre-warming for the current offering; tests were updated/added to cover these behaviors, plus a `NoOpLogHandler` for silencing logs in tests. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ef23ab0. 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 Sonnet 4.6 <noreply@anthropic.com>

Motivation
A workflow paywall pre-rendering approach was retired in favor of a two-surface slide model. Holding every step in the slot table at once was wasteful (most are off-screen) and the prior animation needed
snapTo()+withFrameNanos()workarounds to avoid first-frame flashes.Description
Only the current step and the outgoing/incoming step are held in the slot table during a transition; all other steps are dropped.
Key design:
WorkflowPaywallUiStatecarries apendingTransition(fromStepId,direction, monotonicid) set atomically withcurrentStepIdin the ViewModel. The first recomposition after navigation already knows both surfaces and their initial positions.key(pendingTransition.id)creates a freshAnimatable(0f)during the composition phase, so frame N's draw immediately sees the correct offscreen position — nosnapTo()orwithFrameNanos()needed.LaunchedEffectonly drivesanimateTo(1f); it no longer sets up state.onTransitionComplete(id)runs after the animation finishes so the ViewModel can clearpendingTransition; a guard onidprevents stale callbacks from clobbering a newer transition.seenStepIdgap-detection;pendingTransitioncovers both the "before animation starts" and "animating" cases with the same branch.navigationDirectionremoved from thePaywallViewModelinterface; direction lives exclusively inWorkflowPendingTransition.Image cache pre-warming for the off-screen steps is split into a follow-up PR (#3421) so this PR stays focused on the surface model and animation.
Checklist
LoadedWorkflowPaywallHeaderSelectionTest)purchases-iosand hybridsNote
Medium Risk
Introduces new workflow-specific rendering and animation state for Components paywalls, which can affect navigation, touch handling, and header/hero layout during transitions. Risk is mostly UI/UX regressions (flashes, clipping, wrong header selection) rather than data/security concerns.
Overview
Adds a dedicated workflow paywall renderer that uses a two-surface slide transition:
InternalPaywallnow rendersLoadedWorkflowPaywallwhenworkflowStateis present, otherwise it falls back toLoadedPaywallComponents.Implements
WorkflowSlideStateto drive horizontal slide animations keyed bypendingTransition.id(keeping only the current + outgoing/incoming steps in composition) and adds header-selection logic to keep the correct header visible during hero/non-hero and backward transitions, covered by new unit tests (LoadedWorkflowPaywallHeaderSelectionTest).Reviewed by Cursor Bugbot for commit 88595b3. Bugbot is set up for automated code reviews on this repo. Configure here.