Skip to content

Add slide transition to workflow paywalls#3418

Merged
vegaro merged 8 commits into
mainfrom
cesar/wfl-46-two-surface-workflow-slide
May 4, 2026
Merged

Add slide transition to workflow paywalls#3418
vegaro merged 8 commits into
mainfrom
cesar/wfl-46-two-surface-workflow-slide

Conversation

@vegaro

@vegaro vegaro commented Apr 30, 2026

Copy link
Copy Markdown
Member

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:

  • WorkflowPaywallUiState 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) runs 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 the PaywallViewModel interface; direction lives exclusively in WorkflowPendingTransition.

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

  • Unit tests added (LoadedWorkflowPaywallHeaderSelectionTest)
  • If applicable, create follow-up issues for purchases-ios and hybrids

Note

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: InternalPaywall now renders LoadedWorkflowPaywall when workflowState is present, otherwise it falls back to LoadedPaywallComponents.

Implements WorkflowSlideState to drive horizontal slide animations keyed by pendingTransition.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.

@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from f454df4 to 9e6183f Compare April 30, 2026 08:50
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch 2 times, most recently from 7b4fbec to f3f173e Compare April 30, 2026 08:55
@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from 9e6183f to 1f8ba59 Compare April 30, 2026 08:55
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch 2 times, most recently from 5e42321 to dc3b6e9 Compare April 30, 2026 09:10
@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.45%. Comparing base (dbd69ef) to head (88595b3).
⚠️ Report is 1 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from 1f8ba59 to e72fe0f Compare April 30, 2026 09:25
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch 4 times, most recently from 46fd66d to 080ad91 Compare April 30, 2026 09:46
@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from e72fe0f to b935b86 Compare April 30, 2026 09:48
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch from 080ad91 to 8c45648 Compare April 30, 2026 09:49
@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch 2 times, most recently from 241a0f1 to ebc6b1f Compare April 30, 2026 10:12
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch from 8c45648 to 553bfb7 Compare April 30, 2026 10:12
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch from 553bfb7 to e4e3bc9 Compare April 30, 2026 11:01
@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from ebc6b1f to d71ea2c Compare April 30, 2026 11:01
@vegaro vegaro force-pushed the cesar/wfl-46-paywall-scaffold branch from e4e3bc9 to fbf8377 Compare April 30, 2026 11:31
@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from d71ea2c to 2e368bc Compare April 30, 2026 11:31
@vegaro vegaro requested a review from facumenzella May 4, 2026 13:08
Base automatically changed from cesar/wfl-46-paywall-scaffold to main May 4, 2026 13:21
vegaro and others added 6 commits May 4, 2026 15:22
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>
@vegaro vegaro force-pushed the cesar/wfl-46-two-surface-workflow-slide branch from 3112a77 to f63f313 Compare May 4, 2026 13:22
@vegaro vegaro changed the title feat: two-surface slide animation for workflow paywalls Add slide transition to workflow paywalls May 4, 2026
Comment on lines +27 to +29
internal data class WorkflowHeaderTransitionState(
val pendingTransition: WorkflowPendingTransition?,
)

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.

Will this include more val in the near future?

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

I think it makes sense. Looking forward to testing again once everything is merged 👍

@vegaro vegaro enabled auto-merge May 4, 2026 15:16
@vegaro vegaro added this pull request to the merge queue May 4, 2026
Merged via the queue into main with commit 8383c70 May 4, 2026
37 checks passed
@vegaro vegaro deleted the cesar/wfl-46-two-surface-workflow-slide branch May 4, 2026 16:17

@tonidero tonidero left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems good to me. Just a very small comment.

Comment on lines +38 to +39
val visibleStepIds: Set<String>,
val animatingFromStepId: String?,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
**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 -->
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
### 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>
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.

3 participants