Skip to content

Add checkout diagnostics for Swift - #642

Open
markmur wants to merge 1 commit into
mainfrom
swift-checkout-diagnostics
Open

Add checkout diagnostics for Swift#642
markmur wants to merge 1 commit into
mainfrom
swift-checkout-diagnostics

Conversation

@markmur

@markmur markmur commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

What changes are you making?

Introduce SDK-wide Swift diagnostics subscriptions and move incoming checkout-message trust decisions out of Configuration callbacks.

  • Add ShopifyCheckoutKit.diagnostics.subscribe { ... } with a retained, cancellable subscription handle.
  • Deliver listeners on the main actor to match checkout preload observability.
  • Add CheckoutDiagnosticEvent.messageRejected with typed CheckoutMessageRejection.Reason values.
  • Add CheckoutMessageIngressPolicy so WebKit transport metadata is admitted before protocol dispatch.
  • Remove Configuration.onMessageRejected and the raw rejected message body from the public API.
  • Preserve debug logging when no consumer observes diagnostics.
  • Document why rejected payloads are intentionally omitted and why protocol clients only receive admitted messages.

How message admission works

CheckoutMessageIngressPolicy is the trust boundary between WebKit transport and checkout protocol handling. WebKit provides the authoritative source origin, frame, and request URL alongside the untrusted message body. The WebView packages those values into an IncomingCheckoutMessage and asks the policy for an .accepted or .rejected(...) decision before attempting to parse or dispatch the protocol message.

The policy applies the transport-level rules in one place:

  • Reject messages sent by a child frame.
  • Build the effective origin allowlist from allowedMessageOrigins and the loaded checkout URL.
  • When origin validation is enabled, reject an explicitly specified port zero and origins that do not match the allowlist.
  • Accept the message when all enabled admission checks pass.

An accepted message continues into CheckoutProtocol parsing and client dispatch. A rejected message never reaches the protocol client; the WebView emits .messageRejected with the trusted origin metadata and typed rejection reason, then stops processing it. This lets the protocol client assume it only handles admitted checkout messages without making it responsible for WebKit-specific trust decisions.

Consumer API

Consumers retain one SDK-wide subscription for as long as they want to observe diagnostics:

let diagnosticsSubscription = ShopifyCheckoutKit.diagnostics.subscribe { event in
    guard case let .messageRejected(rejection) = event else { return }

    reportRejectedMessage(
        origin: rejection.origin,
        reason: rejection.reason
    )
}

// Cancel when the observing component is destroyed.
diagnosticsSubscription.cancel()

Subscriptions are hot and do not replay. Subscribe before calling preload(checkout:) if the application needs diagnostics emitted by a background checkout WebView. Listeners are delivered on the main actor, and releasing the subscription also stops observation.

The raw rejected message body is intentionally unavailable because it is untrusted and may contain sensitive or arbitrarily large data.

React Native consumer API

React Native would expose the same typed subscription model without requiring async/await or direct access to the native module:

const diagnosticsSubscription = checkout.diagnostics.subscribe(event => {
  if (event.type === 'messageRejected') {
    reportRejectedMessage(
      event.rejection.origin,
      event.rejection.reason,
    );
  }
});

// Stop observing when the owning component is destroyed.
diagnosticsSubscription.remove();

The event is a typed CheckoutDiagnosticEvent, with messageRejected carrying the rejected origin and a childFrame, unsupportedPort, or originNotAllowed reason. Subscriptions are hot and non-replaying, matching Swift and Kotlin behavior.


Before you merge

Important

  • I've added tests to support my implementation
  • I have read and agree with the Contribution Guidelines
  • I have read and agree with the Code of Conduct
  • I've updated the relevant platform README (platforms/swift/README.md and/or platforms/android/README.md)

@markmur
markmur requested a review from a team as a code owner August 14, 2026 09:25
@github-actions github-actions Bot added the #gsd:50662 Rebase Checkout Kit on UCP label Aug 14, 2026

markmur commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@markmur markmur changed the title Add Swift checkout diagnostics Add checkout diagnostics for Swift Aug 14, 2026
@bitrise

bitrise Bot commented Aug 14, 2026

Copy link
Copy Markdown

Install this build

Open Tophat, select your target device, then click Install. Links open on the Mac running Tophat.

SDK Install
Swift Install with Tophat

Checkout Kit E2E results

Status Suite Target Platform OS version tag Device
swift-ios swift ios latest iPhone 15
iOS 27 Beta

@markmur
markmur force-pushed the swift-checkout-diagnostics branch from 0733f3e to b19daf0 Compare August 14, 2026 09:55
Comment on lines +49 to +53
/// Keep the subscription for as long as diagnostics should be observed.
/// Observation stops when the subscription is cancelled or released.
@MainActor
public final class Subscription {
private var listener: (@MainActor (CheckoutDiagnosticEvent) -> Void)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was tempted to implement an AsyncStream here instead but decided to align with the existing subscribe pattern for preload observability. Also, implementing async streams in Kotlin required the coroutines dependency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:50662 Rebase Checkout Kit on UCP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant