Skip to content

Wire workflow step lifecycle events in PaywallViewModel#3487

Merged
vegaro merged 12 commits into
mainfrom
cesar/workflow-events-wiring
Jun 8, 2026
Merged

Wire workflow step lifecycle events in PaywallViewModel#3487
vegaro merged 12 commits into
mainfrom
cesar/workflow-events-wiring

Conversation

@vegaro

@vegaro vegaro commented May 14, 2026

Copy link
Copy Markdown
Member

Stacked on #3486.

Wires WorkflowEvent tracking into PaywallViewModelImpl:

  • StepStarted on initial step display and on every forward/back navigation
  • StepCompleted when navigating away from a step, on dismiss, and on error during step transition
  • Extracts rebuildWorkflowStepStates() to handle color scheme changes without re-triggering navigation events

Note

Medium Risk
Touches paywall dismiss, purchase, and restore flows alongside new analytics; behavior is heavily tested but incorrect event timing could skew workflow funnels.

Overview
PaywallViewModelImpl now emits WorkflowEvent.StepStarted and WorkflowEvent.StepCompleted during multi-step workflow paywalls, using a per-impression workflowTraceId and entry reasons (start, forward, back).

Events fire on initial step load, forward/back navigation, user close, step transitions that error, and when leaving a step without another step (purchase/restore dismiss paths call trackCurrentWorkflowStepCompleted without adding PaywallEventType.CLOSE). Workflow bootstrap APIs are renamed (startWorkflowPresentation, startWorkflowPresentationFromResult), and buildWorkflowStates takes isNewWorkflowImpression so color-scheme rebuilds refresh step UI without extra StepStarted events. Fallback package-step prebuild can skip applying UI state via shouldApplyState.

Tests cover workflow event payloads, trace IDs across re-presentations, error paths, and regressions that purchase/restore dismiss still do not emit paywall close events.

Reviewed by Cursor Bugbot for commit 35b1495. Bugbot is set up for automated code reviews on this repo. Configure here.

vegaro commented May 14, 2026

Copy link
Copy Markdown
Member Author

@vegaro vegaro changed the title Rebuild workflow step states on color scheme change (#3419) feat: wire workflow step lifecycle events in PaywallViewModel May 14, 2026
@vegaro vegaro changed the title feat: wire workflow step lifecycle events in PaywallViewModel Wire workflow step lifecycle events in PaywallViewModel May 14, 2026
@vegaro vegaro force-pushed the cesar/workflow-events-wiring branch from ed4537f to f447516 Compare May 14, 2026 16:30
@vegaro vegaro force-pushed the cesar/workflow-events-definition branch from be17d14 to 3f91e95 Compare May 14, 2026 16:30
@vegaro vegaro marked this pull request as ready for review May 14, 2026 16:30
@vegaro vegaro requested a review from a team as a code owner May 14, 2026 16:30
@vegaro vegaro force-pushed the cesar/workflow-events-wiring branch from f447516 to 334a5be Compare May 28, 2026 01:31
@vegaro vegaro force-pushed the cesar/workflow-events-wiring branch from c90c630 to 2168068 Compare May 28, 2026 17:38
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.11%. Comparing base (27fd84b) to head (35b1495).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3487   +/-   ##
=======================================
  Coverage   80.11%   80.11%           
=======================================
  Files         371      371           
  Lines       15166    15166           
  Branches     2100     2100           
=======================================
  Hits        12150    12150           
  Misses       2166     2166           
  Partials      850      850           

☔ View full report in Codecov by Harness.
📢 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.

@@ -738,7 +750,7 @@ internal class PaywallViewModelImpl(
updatePaywallState(currentOffering)
}

private suspend fun updateStateFromWorkflowEndpointIfNeeded(offeringSelection: OfferingSelection): Boolean {
private suspend fun startWorkflowPresentationFromEndpointIfNeeded(offeringSelection: OfferingSelection): Boolean {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Renamed this one and updateStateFromWorkflow becuase I found the naming confusing

@vegaro vegaro requested a review from facumenzella May 28, 2026 18:48
Base automatically changed from cesar/workflow-events-definition to main May 28, 2026 22:39
vegaro and others added 8 commits May 28, 2026 15:55
In a multi-step workflow paywall, toggling dark/light mode while on a
non-first step would silently navigate the user back to step 1. The
configuration change calls `updateState()`, which re-runs the full
workflow setup — including resetting `WorkflowNavigator` to the initial
step — instead of just rebuilding the cached step states for the new
color scheme.

Introduces `rebuildWorkflowStepStates()`, called from the
configuration-change path instead of `updateState()` when the active
paywall is a workflow. It:

- Clears the existing `workflowStepStateCache`.
- Rebuilds the current step's `PaywallState.Loaded.Components` against
the new color scheme.
- Leaves `WorkflowNavigator`'s `currentStepId` and `backStack`
untouched, so the user stays on the step they were on.
- Re-kicks the off-thread pre-warm so visited and unvisited steps
repopulate with the new colors.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes workflow paywall refresh behavior to rebuild step UI state
in-place instead of reloading the workflow, which could impact
navigation/state caching if edge cases exist. Covered by new unit tests
that assert navigation position and network call count are preserved.
>
> **Overview**
> Fixes a workflow-paywall bug where changing the Compose `ColorScheme`
(e.g., dark/light toggle) could reset multi-step workflow navigation
back to the initial step.
>
> When a workflow is active, `refreshStateIfColorsChanged` now rebuilds
workflow step states via
`rebuildWorkflowStepStates`/`buildWorkflowStates` (clearing and
repopulating the step cache using the *current* step) instead of calling
`updateState()` and re-fetching/resetting the `WorkflowNavigator`. Adds
tests ensuring the current workflow step is preserved and that
`awaitGetWorkflow` is not called again on color refresh.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
8472463. 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>
Co-authored-by: Facundo Menzella <facumenzella@gmail.com>

feat: track workflow step lifecycle events (step_started / step_completed)

Implements the workflows_step_started and workflows_step_completed event
pipeline, routing WorkflowEvent through EventsManager → BackendStoredEvent
→ BackendEvent.Workflows → /v1/events.

- Add WorkflowEvent sealed class (StepStarted / StepCompleted) implementing
  FeatureEvent; @SerialName discriminators ensure stable serialisation
- Add BackendEvent.Workflows wire model with nested Context and Properties
- Register BackendStoredEvent.Workflows in both JsonProvider and
  EventsManager polymorphic modules
- Add WorkflowEvent.toBackendStoredEvent() converter
- Track step_started on successful initial workflow load (guarded so a
  failed load does not fire spurious events)
- Track step_completed + step_started on forward and back navigation
- Track step_completed (toStepId = null) on paywall close
- Track step_completed and clear traceId when workflow state is cleared
  on error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

feat: align workflow event properties with monetization event naming spec

Remove trace_id (not in spec) and add workflow_type ("paywall"),
step_type (from WorkflowStep.type), and screen_type (from
WorkflowStep.screenType, defaults to emptyList) to both
StepStarted and StepCompleted events and their BackendEvent wire shape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

test: add non-empty screen_type coverage for workflow events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix: remove workflow_type/step_type/screen_type from BackendEvent wire model

Khepri derives these from its own DB at event time — no need for the SDK to send them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… trackWorkflowStepStarted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ents

Generate a UUID per workflow impression in buildWorkflowStates and pass
it as trace_id to every StepStarted/StepCompleted for that impression.
Each call to buildWorkflowStates (new impression) generates a fresh UUID
so distinct impressions are separately correlated.

Also removes workflowType/stepType/screenType from event construction —
these are derived server-side from the workflow/step IDs and must not be
sent by the client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaro vegaro force-pushed the cesar/workflow-events-wiring branch from 530f3d9 to ccc70e0 Compare May 28, 2026 22:55
…g paywall_close

- Fire WorkflowEvent.StepCompleted when leaving the current step on dismiss,
  purchase (both billing modes), and restore-dismiss, via a shared
  trackCurrentWorkflowStepCompleted() helper.
- Revert the RevenueCat purchase path to options.dismissRequest(): routing it
  through closePaywall() was emitting a spurious paywall_close on every
  successful purchase. paywall_close now fires in exactly the same cases as
  before workflows.
- Add regression tests asserting RevenueCat purchase and restore-dismiss do not
  emit paywall_close.
- Assert isFirstStep/isLastStep on workflow lifecycle events, add a MY_APP
  purchase parity test, and fix a colliding test fixture id.
- Make currentWorkflowStep a val with a custom getter (no-arg derived-state read).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@facumenzella

facumenzella commented Jun 4, 2026

Copy link
Copy Markdown
Member

Porting this to iOS (RevenueCat/purchases-ios#6868) I went back to the Spec and noticed the naming doesn't line up with what we're sending here

The Spec wants singular rc_workflow_step_started_event / ..._completed_event (and dropping the workflow purchase event in favor of rc_checkout_purchase). But we're sending plural workflows_step_started / workflows_step_completed, and Khepri still pins those exact plural names as Literal types and keeps WORKFLOWS_PURCHASE in the enum.

matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
Adds the domain model and serialization layer for workflow step
lifecycle events. Wiring in `PaywallViewModel` is in RevenueCat#3487.

- `WorkflowEvent` sealed class (`StepStarted`, `StepCompleted`), sibling
to `PaywallEvent`
- `BackendEvent.Workflows` wire format matching khepri's
`WorkflowsEvent` schema
- `BackendStoredEvent.Workflows` +
`WorkflowEvent.toBackendStoredEvent()` mapper
- `BackendStoredEvent.Workflows` registered in `EventsManager`'s JSON
polymorphic config

Definition in
https://www.notion.so/revenuecat/Monetization-Event-Naming-Spec-32ccb1a108b0816d9a3efcf8f26813e4?source=copy_link


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Additive event-schema and plumbing following existing
paywall/custom-paywall patterns; no auth or purchase logic changes, with
coverage for JSON shape and persistence.
> 
> **Overview**
> Introduces **workflow step lifecycle analytics**
(`WorkflowEvent.StepStarted` / `StepCompleted`) and wires them through
the same path as paywall and customer-center events.
> 
> Domain events map to a new **`BackendEvent.Workflows`** payload
(khepri-compatible JSON with `event_name`, `properties`, optional
`trace_id`, etc.), persist as **`BackendStoredEvent.Workflows`**, and
are registered in **`JsonProvider`**, **`EventsManager`** polymorphic
JSON, and **`track()`** so they land in the event store and flush with
other backend events. **PaywallViewModel** emission is explicitly out of
scope (follow-up PR).
> 
> Tests cover conversion, on-disk storage via `EventsManager`, and
request JSON shape (including fields that must not appear in output).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
366a898. 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>

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

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 0c75a02. Configure here.

…flowImpression

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaro

vegaro commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@facumenzella in the spec there's a work required "2.2 Fix singular/plural naming inconsistency and remove purchase event" that talks about that

Calling startWorkflowPresentation while a workflow step is already active
dropped the prior step's StepCompleted event, leaving its lifecycle open.
Call trackCurrentWorkflowStepCompleted() before mutating state so the old
trace ID and step ID are still available when the event is emitted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vegaro

vegaro commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@facumenzella added some extra tests. I am going to merge this.

@vegaro vegaro enabled auto-merge June 8, 2026 16:31
@vegaro vegaro added this pull request to the merge queue Jun 8, 2026
Merged via the queue into main with commit 1b6280d Jun 8, 2026
39 checks passed
@vegaro vegaro deleted the cesar/workflow-events-wiring branch June 8, 2026 17:06
facumenzella added a commit to RevenueCat/purchases-ios that referenced this pull request Jun 9, 2026
…del (#6868)

* feat(workflows): wire workflow step lifecycle events in the paywall

Port of RevenueCat/purchases-android#3487

Emits WorkflowEvent.stepStarted / stepCompleted during multi-step workflow
paywalls, building on the model and wire format added in #6858.

- Adds Purchases.track(workflowEvent:) and routes it through the existing
  FeatureEvent path, exposed via PaywallPurchasesType and PurchaseHandler
- Adds WorkflowStepEventTracker: builds events with a per-impression traceId,
  entry reasons (start/forward/back), and terminal-step detection
- Wires WorkflowPaywallView to emit at four points: initial step, forward,
  back, and terminal completion (anchored to onDisappear like paywall_close)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(workflows): don't emit terminal stepCompleted when no page rendered

trackTerminalCompletionIfNeeded fired on dismiss whenever navigator.currentStep
was non-nil, even if no page ever rendered. That emitted a stepCompleted with no
preceding stepStarted in two cases: the initial step failing to build, and a
forward destination failing to build (the navigator advances but the page does not).

Gate terminal completion on transitionState.currentPage != nil, the same render
guard trackInitialStepIfNeeded already uses. This mirrors Android keying terminal
completion off _workflowState.value?.currentStepId, which is null when a step fails
to render. Extracted the decision into a pure shouldTrackTerminalCompletion helper
and unit tested it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(workflows): extract WorkflowStepEventCoordinator for testable emission

The step event emission state (trace id, fire-once flags, "only if a page
rendered" gating) lived inline in WorkflowPaywallView, so the emission
sequence could only be verified manually. Android tests the equivalent logic
at the PaywallViewModel level; this brings iOS to parity.

Move that state machine into WorkflowStepEventCoordinator (composing the
existing WorkflowStepEventTracker). The view holds it as @State created in
init, so a new presentation yields a fresh traceId, and its four lifecycle
and navigation hooks become one-line delegations. Behavior is unchanged: the
currentPage gate, fire-once semantics, and forward/back/error/terminal
emission points all match the prior inline logic.

Add WorkflowStepEventCoordinatorTests covering the full appear/forward/back/
dismiss sequence, fire-once and no-page gating, traceId continuity and
per-impression reset, and a journey asserted at the MockPurchases
track(workflowEvent:) boundary (content/count, since the dispatcher does not
guarantee delivery order). The three bool-helper terminal tests in
WorkflowPaywallViewTests are migrated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(workflows): forward workflow events through MockPurchases.map wrappers

The map(purchase:restore:) and map(trackEvent:) extensions forwarded the
workflow fetch block but not trackWorkflowEventBlock, so a mapped copy's
track(workflowEvent:) silently dropped events. This is DEBUG-only (MockPurchases
is #if DEBUG) and reachable via PurchaseHandler.cancelling()/.with(delay:), so
production analytics were unaffected, but it could drop events in the
PaywallsTester debug-log path used to verify this feature.

Forward trackWorkflowEventBlock to the original, mirroring how workflowBlock and
the paywall trackEvent block are already forwarded. Flagged by Cursor Bugbot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(workflows): overload track(_:) for WorkflowEvent

Rename trackWorkflow/trackWorkflowEvent to track(_:), overloading by event
type alongside the existing track(_ PaywallEvent) methods. Addresses review
feedback on PaywallEventTracker and PurchaseHandler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

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.

2 participants