Skip to content

feat(workflows): add WorkflowEvent model and wire format serialization#6858

Merged
facumenzella merged 9 commits into
mainfrom
port/3486
May 29, 2026
Merged

feat(workflows): add WorkflowEvent model and wire format serialization#6858
facumenzella merged 9 commits into
mainfrom
port/3486

Conversation

@facumenzella

@facumenzella facumenzella commented May 27, 2026

Copy link
Copy Markdown
Member

Port of RevenueCat/purchases-android#3486

Summary

  • Adds WorkflowEvent (stepStarted/stepCompleted) as the iOS domain model for workflow step lifecycle events, sibling to PaywallEvent
  • Adds FeatureEventsRequest.WorkflowEvent as 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)
  • Adds .workflows case to Feature enum and routes it through FeatureEventsRequest
  • Adds screenType field to WorkflowStep for local context (not sent to backend)
  • Fields workflowType, stepType, screenType on WorkflowEvent.Data are local-only and excluded from wire format

Test plan

  • WorkflowEventTests: stepStarted/stepCompleted carry correct fields, feature/eventDiscriminator, round-trip via StoredFeatureEvent
  • WorkflowEventsRequestTests: wire format JSON shape, event_name mapping per case, local-only fields absent from JSON, nil optionals absent
  • EventsManagerTests/testTrackWorkflowEventStoresWithWorkflowsFeature: tracking stores event with .workflows feature

Note: The UnitTests Xcode 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: stepStarted and stepCompleted events that plug into the existing feature-events store and flush path, aligned with the Android port.

The new internal WorkflowEvent model mirrors PaywallEvent and maps to hybrid toMap() output (workflows discriminator, workflows_step_started / workflows_step_completed). A .workflows Feature case routes stored events through FeatureEventsRequest into a Khepri-compatible wire type (event_name, timestamp_ms, app_user_id, context.locale, and step/navigation/experiment properties). Step-started events carry from_step_id and entry_reason; step-completed carry to_step_id. Fields like workflowType, stepType, and screenType stay on the domain model for local use only and are omitted from backend JSON.

WorkflowStep decoding gains screenType (exposed as stepScreenType) for local context. Unit tests cover model behavior, wire encoding, storage with .workflows, and toMap() optional fields.

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

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>
@facumenzella facumenzella added the pr:feat A new feature label May 27, 2026
@emerge-tools

emerge-tools Bot commented May 27, 2026

Copy link
Copy Markdown

4 builds increased size

Name Version Download Change Install Change Approval
RevenueCat
com.revenuecat.PaywallsTester
1.0 (1) 18.2 MB ⬆️ 60.8 kB (0.34%) 65.6 MB ⬆️ 253.4 kB (0.39%) N/A
BinarySizeTest
com.revenuecat.binary-size-test.local-source
1.0 (1) 4.2 MB ⬆️ 21.7 kB (0.53%) 12.5 MB ⬆️ 61.4 kB (0.5%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.cocoapods
1.0 (1) 6.3 MB ⬆️ 31.9 kB (0.51%) 27.6 MB ⬆️ 153.3 kB (0.56%) ⏳ Needs approval
BinarySizeTest
com.revenuecat.binary-size-test.spm
1.0 (1) 4.3 MB ⬆️ 21.7 kB (0.51%) 10.9 MB ⬆️ 52.6 kB (0.49%) ⏳ Needs approval

RevenueCat 1.0 (1)
com.revenuecat.PaywallsTester

⚖️ Compare build
⏱️ Analyze build performance

Total install size change: ⬆️ 253.4 kB (0.39%)
Total download size change: ⬆️ 60.8 kB (0.34%)

Largest size changes

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
View Treemap

Image of diff

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
View Treemap

Image of diff

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
View Treemap

Image of diff

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
View Treemap

Image of diff


🛸 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>
@facumenzella facumenzella marked this pull request as ready for review May 27, 2026 18:10
@facumenzella facumenzella requested a review from a team as a code owner May 27, 2026 18:10
Comment thread Sources/Paywalls/Events/WorkflowEvent.swift
Comment thread Tests/UnitTests/Paywalls/Events/WorkflowEventTests.swift Outdated
Comment thread Sources/Paywalls/Events/WorkflowEvent.swift
Comment thread Sources/Events/FeatureEvents/Networking/FeatureEventsRequest+WorkflowEvent.swift Outdated
Comment thread Sources/Events/FeatureEvents/Networking/FeatureEventsRequest+WorkflowEvent.swift Outdated
Comment thread Sources/Events/FeatureEvents/Networking/FeatureEventsRequest+WorkflowEvent.swift Outdated
Comment thread Sources/Events/FeatureEvents/Networking/FeatureEventsRequest+WorkflowEvent.swift Outdated
Comment thread Sources/Events/FeatureEvents/Networking/FeatureEventsRequest+WorkflowEvent.swift Outdated
- 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>
@facumenzella facumenzella requested a review from vegaro May 28, 2026 05:19
Comment thread Sources/Events/FeatureEvents/Networking/FeatureEventsRequest+WorkflowEvent.swift Outdated
Comment thread Sources/Networking/Responses/WorkflowsResponse.swift
…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 vegaro 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.

good to go after those suggestions

Comment thread Sources/Events/FeatureEvents/FeatureEvent.swift
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 45434f9. Configure here.

Comment thread Sources/Events/FeatureEvents/FeatureEvent.swift
@facumenzella facumenzella enabled auto-merge (squash) May 29, 2026 06:15
@facumenzella facumenzella disabled auto-merge May 29, 2026 06:22
…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>
@facumenzella facumenzella enabled auto-merge (squash) May 29, 2026 06:25
@facumenzella facumenzella merged commit d54e876 into main May 29, 2026
17 of 20 checks passed
@facumenzella facumenzella deleted the port/3486 branch May 29, 2026 06:43
facumenzella added a commit 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

Labels

pr:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants