feat(workflows): add WorkflowEvent model and wire format serialization#6858
Conversation
Port of RevenueCat/purchases-android#3486 Adds WorkflowEvent (stepStarted/stepCompleted) as the iOS domain model for workflow step lifecycle events. Adds FeatureEventsRequest.WorkflowEvent as the Khepri-compatible wire format, wires it into the Feature routing pipeline, and exposes screenType on WorkflowStep for local context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 builds increased size
RevenueCat 1.0 (1)
|
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 88.0 kB |
| RevenueCat.InternalAPI.InternalAPI | ⬆️ 8.8 kB |
| Code Signature | ⬆️ 6.2 kB |
| DYLD.Exports | ⬆️ 4.7 kB |
| 📝 RevenueCat.WorkflowEvent.Data.Swift Metadata | ⬆️ 650 B |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 61.4 kB (0.5%)
Total download size change: ⬆️ 21.7 kB (0.53%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 6.8 kB |
| 📝 RevenueCat.WorkflowEvent.workflowEventMap | ⬆️ 3.5 kB |
| 📝 RevenueCat.WorkflowEvent.Data.init(from) | ⬆️ 3.3 kB |
| 📝 RevenueCat.FeatureEventsRequest.WorkflowEvent.init(storedEvent) | ⬆️ 3.0 kB |
| DYLD.Exports | ⬆️ 2.8 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 153.3 kB (0.56%)
Total download size change: ⬆️ 31.9 kB (0.51%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 70.6 kB |
| Code Signature | ⬆️ 4.0 kB |
| 📝 RevenueCat.WorkflowEvent.workflowEventMap | ⬆️ 3.5 kB |
| 📝 RevenueCat.WorkflowEvent.Data.init(from) | ⬆️ 3.3 kB |
| 📝 RevenueCat.FeatureEventsRequest.WorkflowEvent.init(storedEvent) | ⬆️ 3.0 kB |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 52.6 kB (0.49%)
Total download size change: ⬆️ 21.7 kB (0.51%)
Largest size changes
| Item | Install Size Change |
|---|---|
| 📝 RevenueCat.WorkflowEvent.workflowEventMap | ⬆️ 3.5 kB |
| 📝 RevenueCat.WorkflowEvent.Data.init(from) | ⬆️ 3.3 kB |
| 📝 RevenueCat.FeatureEventsRequest.WorkflowEvent.init(storedEvent) | ⬆️ 3.0 kB |
| DYLD.Exports | ⬆️ 2.8 kB |
| 📝 RevenueCat.WorkflowEvent.value witness | ⬆️ 2.2 kB |
🛸 Powered by Emerge Tools
Comment trigger: Size diff threshold of 100.00kB exceeded
Adds WorkflowEvent.swift, FeatureEventsRequest+WorkflowEvent.swift, WorkflowEventTests.swift, and WorkflowEventsRequestTests.swift to the committed SPM-generated Xcode project so CI builds pick them up. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename `discriminator` to `type` in FeatureEventsRequest.WorkflowEvent (matches backend expectation and CustomPaywallEvent pattern) - Add `version: Int = 1` to wire format, matching Android and CustomPaywallEvent schema versioning - Add `experimentId`, `experimentVariant`, `isLastVariantStep` to WorkflowEvent.Data and wire format Properties (parity with Android/Khepri) - Add `traceId` to WorkflowEvent.Data and wire format Properties (parity with Android) - Update tests to cover renamed field, version, new experiment fields, and traceId in JSON shape Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…test extension - Add WorkflowEvent case to FeatureEvent.toMap() so eventsListener receives structured data instead of the unknown fallback - Remove redundant private extension in WorkflowEventTests that shadowed the @_spi(Internal) public creationData/data accessors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vegaro
left a comment
There was a problem hiding this comment.
good to go after those suggestions
…ields, screenType accessor - Add localeIdentifier to WorkflowEvent.Data (captured at creation, always sent in wire format context) - Change Context.locale from String? to String; wire format always includes locale - Add trace_id, experiment_id, experiment_variant, is_last_variant_step to workflowEventMap() - Add public stepScreenType accessor on WorkflowStep for SPI consumers - Add tests for locale in wire format and all workflowEventMap() fields 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 45434f9. Configure here.
…onsistency Matches paywallMap(), customerCenterImpressionMap(), and customerCenterAnswerSubmittedMap() which all include locale. Hybrid SDK consumers now receive locale for workflow events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>





Port of RevenueCat/purchases-android#3486
Summary
WorkflowEvent(stepStarted/stepCompleted) as the iOS domain model for workflow step lifecycle events, sibling toPaywallEventFeatureEventsRequest.WorkflowEventas the Khepri-compatible wire format with discriminator, event_name, timestamp_ms, app_user_id, context (locale), and properties (workflow_id, step_id, from_step_id, to_step_id, entry_reason, is_first_step, is_last_step).workflowscase toFeatureenum and routes it throughFeatureEventsRequestscreenTypefield toWorkflowStepfor local context (not sent to backend)workflowType,stepType,screenTypeonWorkflowEvent.Dataare local-only and excluded from wire formatTest plan
WorkflowEventTests: stepStarted/stepCompleted carry correct fields, feature/eventDiscriminator, round-trip via StoredFeatureEventWorkflowEventsRequestTests: wire format JSON shape, event_name mapping per case, local-only fields absent from JSON, nil optionals absentEventsManagerTests/testTrackWorkflowEventStoresWithWorkflowsFeature: tracking stores event with .workflows featureNote: The
UnitTestsXcode scheme has a pre-existing linker issue (RevenueCatUI undefined symbols) unrelated to this change; all new test files compile successfully as shown in the build log.🤖 Generated with Claude Code
Note
Low Risk
Analytics-only extension following established paywall/custom paywall event patterns; no auth, purchases, or runtime workflow execution changes in this diff.
Overview
Adds workflow step lifecycle analytics on iOS:
stepStartedandstepCompletedevents that plug into the existing feature-events store and flush path, aligned with the Android port.The new internal
WorkflowEventmodel mirrorsPaywallEventand maps to hybridtoMap()output (workflowsdiscriminator,workflows_step_started/workflows_step_completed). A.workflowsFeaturecase routes stored events throughFeatureEventsRequestinto a Khepri-compatible wire type (event_name,timestamp_ms,app_user_id,context.locale, and step/navigation/experiment properties). Step-started events carryfrom_step_idandentry_reason; step-completed carryto_step_id. Fields likeworkflowType,stepType, andscreenTypestay on the domain model for local use only and are omitted from backend JSON.WorkflowStepdecoding gainsscreenType(exposed asstepScreenType) for local context. Unit tests cover model behavior, wire encoding, storage with.workflows, andtoMap()optional fields.Reviewed by Cursor Bugbot for commit 5a7eda4. Bugbot is set up for automated code reviews on this repo. Configure here.