Wire workflow step lifecycle events in PaywallViewModel#3487
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
ed4537f to
f447516
Compare
be17d14 to
3f91e95
Compare
f447516 to
334a5be
Compare
c90c630 to
2168068
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
| @@ -738,7 +750,7 @@ internal class PaywallViewModelImpl( | |||
| updatePaywallState(currentOffering) | |||
| } | |||
|
|
|||
| private suspend fun updateStateFromWorkflowEndpointIfNeeded(offeringSelection: OfferingSelection): Boolean { | |||
| private suspend fun startWorkflowPresentationFromEndpointIfNeeded(offeringSelection: OfferingSelection): Boolean { | |||
There was a problem hiding this comment.
Renamed this one and updateStateFromWorkflow becuase I found the naming confusing
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>
530f3d9 to
ccc70e0
Compare
…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>
|
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 |
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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>
|
@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>
|
@facumenzella added some extra tests. I am going to merge this. |
…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>


Stacked on #3486.
Wires
WorkflowEventtracking intoPaywallViewModelImpl:StepStartedon initial step display and on every forward/back navigationStepCompletedwhen navigating away from a step, on dismiss, and on error during step transitionrebuildWorkflowStepStates()to handle color scheme changes without re-triggering navigation eventsNote
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
PaywallViewModelImplnow emitsWorkflowEvent.StepStartedandWorkflowEvent.StepCompletedduring multi-step workflow paywalls, using a per-impressionworkflowTraceIdand 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
trackCurrentWorkflowStepCompletedwithout addingPaywallEventType.CLOSE). Workflow bootstrap APIs are renamed (startWorkflowPresentation,startWorkflowPresentationFromResult), andbuildWorkflowStatestakesisNewWorkflowImpressionso color-scheme rebuilds refresh step UI without extraStepStartedevents. Fallback package-step prebuild can skip applying UI state viashouldApplyState.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.