Skip to content

feat: add event sending#448

Open
chroxify wants to merge 4 commits intodevelopfrom
christo/onboarding-analytics
Open

feat: add event sending#448
chroxify wants to merge 4 commits intodevelopfrom
christo/onboarding-analytics

Conversation

@chroxify
Copy link
Contributor

@chroxify chroxify commented Mar 6, 2026

Changes in this pull request

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs on iOS.
  • Demo project builds and runs on Mac Catalyst.
  • Demo project builds and runs on visionOS.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run swiftlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Greptile Summary

This PR adds multi-page paywall navigation tracking by introducing a page_view web message, a PaywallPageView internal event, and a presentationId correlation ID that flows from getRawPaywall through PaywallPaywallInfo → analytics parameters. The changes follow established patterns for message handling and event tracking.

Key changes:

  • New pageView case on PaywallMessage with 8 associated values (pageNodeId, pageName, pageIndex, navigationNodeId, navigationType, and optional previous-page fields)
  • New InternalSuperwallEvent.PaywallPageView struct that serialises all page fields into analytics params
  • New public SuperwallEvent.paywallPageView and SuperwallEventObjc.paywallPageView cases surfacing the event to SDK consumers
  • presentationId: String? added to Paywall and PaywallInfo, assigned as a fresh UUID per getRawPaywall call, and included in placementParams() as "presentation_id"

Issues to address:

  1. Error-handling inconsistency in PaywallMessage decoding: The .pageView case uses non-optional try for required fields, diverging from the try?/if-let guard pattern used by every other message type. A malformed page_view payload will throw a DecodingError rather than PaywallMessageError.decoding, which callers may not expect.

  2. Limited public API surface for page context: The paywallPageView event only carries paywallInfo, so SDK consumers have no way to determine which page was navigated to, what the navigation type was, or access other page-specific data tracked internally. Compare this to similar rich events like surveyResponse and triggerFire, which embed their contextual data directly.

Confidence Score: 3/5

  • Safe to merge pending fixes to error-handling consistency and public API surface design.
  • Two substantive issues identified that should be addressed before shipping: (1) the error-handling divergence in PaywallMessage decoding could cause unexpected exception types to propagate to callers, and (2) the public event API exposes insufficient context compared to internal tracking and similar rich events, limiting utility for SDK integrators. The core functionality is correct and follows established patterns; the presentationId assignment is properly placed and analytics wiring is complete. However, these two issues represent technical debt and potential API gaps worth resolving.
  • Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessage.swift (error-handling inconsistency), Sources/SuperwallKit/Analytics/Superwall Placement/SuperwallEvent.swift (limited public API surface)

Sequence Diagram

sequenceDiagram
    participant Web as Paywall WebView
    participant MH as PaywallMessageHandler
    participant SW as Superwall.shared
    participant Delegate as SuperwallDelegate

    Web->>MH: page_view message<br/>(pageNodeId, pageIndex, pageName,<br/>navigationNodeId, type, ...)
    MH->>MH: decode into .pageView case<br/>(PaywallMessage.swift)
    MH->>MH: guard delegate, capture paywallInfo
    MH->>SW: Task { track(PaywallPageView event) }
    SW->>SW: getSuperwallParameters()<br/>adds page_node_id, page_index,<br/>page_name, navigation_type,<br/>presentation_id, etc.
    SW->>Delegate: handleSuperwallEvent(.paywallPageView(paywallInfo:))

    Note over Web,Delegate: presentationId is assigned once in getRawPaywall()<br/>and flows through Paywall → PaywallInfo → placementParams
Loading

Last reviewed commit: 4149bce

Greptile also left 2 inline comments on this PR.

case testModeModalClose

/// When a user navigates to a page in a multi-page paywall.
case paywallPageView(paywallInfo: PaywallInfo)
Copy link

Choose a reason for hiding this comment

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

The paywallPageView event only carries paywallInfo as its associated data. SDK consumers receiving this event via the delegate callback will know that a page view occurred, but won't be able to determine which page was viewed, what the navigation type was, or access any other page-specific details.

The internal tracking (lines 262–272 of PaywallMessageHandler.swift) captures all this context—pageNodeId, pageIndex, pageName, navigationType, previousPageNodeId, etc.—but none of it is surfaced in the public event.

Compare this to similar rich events like surveyResponse(survey:selectedOption:customResponse:paywallInfo:) or triggerFire(placementName:result:), which embed their contextual data directly. Consider whether page-specific fields should be included as associated values to make the event useful to SDK integrators:

case paywallPageView(
  paywallInfo: PaywallInfo,
  pageNodeId: String,
  pageIndex: Int,
  pageName: String,
  navigationType: String
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Analytics/Superwall Placement/SuperwallEvent.swift
Line: 285

Comment:
The `paywallPageView` event only carries `paywallInfo` as its associated data. SDK consumers receiving this event via the delegate callback will know that a page view occurred, but won't be able to determine which page was viewed, what the navigation type was, or access any other page-specific details.

The internal tracking (lines 262–272 of `PaywallMessageHandler.swift`) captures all this context—`pageNodeId`, `pageIndex`, `pageName`, `navigationType`, `previousPageNodeId`, etc.—but none of it is surfaced in the public event.

Compare this to similar rich events like `surveyResponse(survey:selectedOption:customResponse:paywallInfo:)` or `triggerFire(placementName:result:)`, which embed their contextual data directly. Consider whether page-specific fields should be included as associated values to make the event useful to SDK integrators:

```swift
case paywallPageView(
  paywallInfo: PaywallInfo,
  pageNodeId: String,
  pageIndex: Int,
  pageName: String,
  navigationType: String
)
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +332 to 352
case .pageView:
let pageNodeId = try values.decode(String.self, forKey: .pageNodeId)
let pageIndex = try values.decode(Int.self, forKey: .pageIndex)
let pageName = try values.decode(String.self, forKey: .pageName)
let navigationNodeId = try values.decode(String.self, forKey: .navigationNodeId)
let previousPageNodeId = try? values.decode(String.self, forKey: .previousPageNodeId)
let previousPageIndex = try? values.decode(Int.self, forKey: .previousPageIndex)
let type = try values.decode(String.self, forKey: .type)
let timeOnPreviousPageMs = try? values.decode(Int.self, forKey: .timeOnPreviousPageMs)
self = .pageView(
pageNodeId: pageNodeId,
pageIndex: pageIndex,
pageName: pageName,
navigationNodeId: navigationNodeId,
previousPageNodeId: previousPageNodeId,
previousPageIndex: previousPageIndex,
type: type,
timeOnPreviousPageMs: timeOnPreviousPageMs
)
return
}
Copy link

Choose a reason for hiding this comment

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

The .pageView case diverges from the established error-handling pattern used by all other message types.

Lines 333–336 and 339 use non-optional try values.decode(...) for required fields (pageNodeId, pageIndex, pageName, navigationNodeId, type). If any of these fields are missing or malformed, this will throw a DecodingError.

Every other case in this decoder (.hapticFeedback, .requestCallback, .stripeCheckoutStart, etc.) uses try? values.decode(...) with an if-let guard. If decoding fails, the guard fails and execution falls through to the catch-all error at line 355, which throws PaywallMessageError.decoding(...).

This inconsistency means a malformed page_view message will propagate a DecodingError instead of the expected PaywallMessageError, which could cause unexpected behavior in callers that specifically catch PaywallMessageError.

For consistency, wrap the required decodes in try? / if-let guards matching the pattern used by .hapticFeedback, .requestCallback, and all other cases.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Paywall/View Controller/Web View/Message Handling/PaywallMessage.swift
Line: 332-352

Comment:
The `.pageView` case diverges from the established error-handling pattern used by all other message types.

Lines 333–336 and 339 use non-optional `try values.decode(...)` for required fields (`pageNodeId`, `pageIndex`, `pageName`, `navigationNodeId`, `type`). If any of these fields are missing or malformed, this will throw a `DecodingError`.

Every other case in this decoder (`.hapticFeedback`, `.requestCallback`, `.stripeCheckoutStart`, etc.) uses `try? values.decode(...)` with an `if-let` guard. If decoding fails, the guard fails and execution falls through to the catch-all error at line 355, which throws `PaywallMessageError.decoding(...)`.

This inconsistency means a malformed `page_view` message will propagate a `DecodingError` instead of the expected `PaywallMessageError`, which could cause unexpected behavior in callers that specifically catch `PaywallMessageError`.

For consistency, wrap the required decodes in `try?` / `if-let` guards matching the pattern used by `.hapticFeedback`, `.requestCallback`, and all other cases.

How can I resolve this? If you propose a fix, please make it concise.

@chroxify chroxify requested review from yusuftor March 10, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant