Skip to content

fix(workflows): pause carousel and video on hidden workflow pages#6913

Draft
facumenzella wants to merge 1 commit into
mainfrom
facu/workflow-pause-hidden-page-media
Draft

fix(workflows): pause carousel and video on hidden workflow pages#6913
facumenzella wants to merge 1 commit into
mainfrom
facu/workflow-pause-hidden-page-media

Conversation

@facumenzella

Copy link
Copy Markdown
Member

Follow-up to #6889. Monika approved it but flagged a real downside: now that every visited workflow step stays mounted off-screen (so its state survives back-navigation), anything time-based on a hidden page keeps running invisibly at opacity 0. A hidden page's onDisappear never fires, so its carousel auto-advance timer keeps ticking and its video keeps playing, burning CPU/battery on a page the user can't see.

This pauses that work while a page is off-screen.

What changed

  • WorkflowPageTransitionContext gets an isPageActive flag (default true, so standalone paywalls are untouched). It's true only for the current and outgoing pages, computed from a new WorkflowPageTransitionState.isPageOnScreen(_:).
  • CarouselView reads the flag and drives its auto-advance timer off it: stop when the page goes off-screen, restart when the user comes back (the index is preserved by @State, so it resumes where it was). onDisappear still stops it for the non-workflow case.
  • VideoComponentView folds the flag into its existing isActiveOrNeighbor playable check, so a video in a carousel on a hidden page pauses for either reason. When it becomes playable again the player is recreated and autoplays as before.

The outgoing page keeps playing through the 0.25s slide and quiesces once the transition completes.

All behind -EnableWorkflowsEndpoint, internal-only.

Out of scope

Monika also mentioned animations. Declarative repeatForever / TimelineView animations on hidden pages aren't gated here (opacity 0 doesn't stop them), and they don't run a timer/player so the cost is lower. Left as a follow-up so this PR stays focused on the two clear offenders.

Verification

  • swift build --target RevenueCatUI: builds.
  • swiftlint on the 6 changed files: 0 violations.
  • Unit tests (swift test): isPageOnScreen (current/outgoing on-screen, hidden not) and the video isPlayable truth table pass; existing CarouselStateTests / VideoComponentViewTests still green.
  • Not device-verified this session: the actual timer-stop / AVPlayer-teardown on a hidden page is SwiftUI-runtime behavior. The wiring is covered by the logic unit tests above; runtime pause should be confirmed in PaywallsTester (-EnableWorkflowsEndpoint, navigate forward then back on a step with an auto-advancing carousel / autoplay video) before merge.
AI session context

Metadata

Goal

Act on the review feedback in #6889 (review) (MonikaMateska, APPROVED): hidden-but-mounted seenPages keep time-based components running at opacity 0, burning CPU/battery.

First actionable instruction

"Check #6889#pullrequestreview-4427637013. Let's follow-up on that." Resolved to: implement a fix that pauses time-based work on hidden workflow pages.

Human decisions

  • Implement in a new PR (vs. plan-only or just replying to Monika).
  • Scope: Carousel + Video only; animations (repeatForever/TimelineView) explicitly out of scope as a follow-up.

Agent contribution

  • Diagnosed root cause: hidden seenPages stay mounted, so onDisappear (where both Carousel and Video stop their work) never fires on navigation.
  • Added isPageActive to WorkflowPageTransitionContext (default true) + WorkflowPageTransitionState.isPageOnScreen(_:); wired in seenPageView.
  • Carousel: pause/restart auto-advance timer off the flag; extracted stopAutoPlay().
  • Video: combined into playable state via a pure isPlayable(isActiveOrNeighbor:isWorkflowPageActive:) helper.
  • Added unit tests for both pure helpers; ran build + lint + targeted tests.

Files / symbols touched

  • RevenueCatUI/Modifiers/EnvironmentValues+Workflow.swift - WorkflowPageTransitionContext.isPageActive + explicit init (the synthesized memberwise init drops a defaulted let, hence the explicit init).
  • RevenueCatUI/Templates/V2/WorkflowPaywallView.swift - WorkflowPageTransitionState.isPageOnScreen(_:) (where Page: Identifiable); seenPageView passes isPageActive.
  • RevenueCatUI/Templates/V2/Components/Carousel/CarouselComponentView.swift - CarouselView reads workflowRenderingContext, timer driven off isPageActive, stopAutoPlay().
  • RevenueCatUI/Templates/V2/Components/Video/VideoComponentView.swift - reads workflowRenderingContext, isPlayable(...) static, resolvePlayableState(), extra onChangeOf.
  • Tests/RevenueCatUITests/PaywallsV2/WorkflowPaywallViewTests.swift - testOnlyCurrentAndOutgoingPagesAreOnScreen + IdentifiablePage; asserted isPageActive default in the identity test.
  • Tests/RevenueCatUITests/PaywallsV2/VideoComponentViewTests.swift - testIsPlayableRequiresBothActiveCarouselPageAndActiveWorkflowPage.

Key implementation decisions

  • Decision: environment flag consumed by components, matching the existing carouselState.isActiveOrNeighbor precedent. Rejected: unmounting hidden pages (defeats the fix(workflows): preserve workflow page state across navigation #6889 state-preservation purpose).
  • Decision: isPageActive = isPageOnScreen (current OR outgoing), so the outgoing page keeps playing through the slide. Rejected: = isCurrent only (would pause the outgoing page mid-transition).
  • Decision: Video combines both conditions (isActiveOrNeighbor && isWorkflowPageActive) so it never overrides the carousel's own pausing.

Validation

  • swift build --target RevenueCatUI: success.
  • swiftlint (6 files): 0 violations.
  • swift test targeted: 3 new + CarouselStateTests (14) + VideoComponentViewTests (3) pass.

Validation gaps

  • No automated test for the SwiftUI runtime pause (timer invalidation / AVPlayer teardown on a hidden page); it is lifecycle-coupled. Covered by logic unit tests; runtime pause not device-verified this session.
  • Hinges on .onChangeOf(isPageActive) firing on the opacity-0 hidden subtree. Expected (the subtree stays live, which is what causes the original bug), but worth confirming on device.

Review focus

  • Is the standalone path (.identity -> isPageActive == true) byte-equivalent to the old video/carousel behavior?
  • Does isPageActive flip at the right moment (outgoing stays active until completeTransition)?
  • On revisit, does the carousel resume from the preserved index and the video recreate its player (playerRefreshToggle)?

Out of scope

  • Animations (repeatForever / TimelineView) on hidden pages.

Workflow keeps every visited step mounted off-screen (seenPages, #6889) so
its state survives back-navigation. The downside MonikaMateska flagged on
that PR: a hidden page's onDisappear never fires, so its carousel auto-advance
timer keeps ticking and its video keeps playing at opacity 0, burning
CPU/battery on a page the user can't see.

Add an isPageActive flag to WorkflowPageTransitionContext (default true, so
standalone paywalls are unaffected), set to true only for the current and
outgoing pages. CarouselView and VideoComponentView read it and quiesce when
inactive: the carousel stops/restarts its timer off the flag instead of
onDisappear, and the video folds it into its existing playable state so a
video in a carousel on a hidden page pauses for either reason.

Behind -EnableWorkflowsEndpoint, internal-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

1 participant