Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
--allman false
--semicolons inline
--trimwhitespace always
--disable redundantReturn,hoistAwait,preferKeyPath,redundantInternal,redundantPublic
--swiftversion 5.7.1
--disable redundantReturn,hoistAwait,preferKeyPath,redundantInternal,conditionalAssignment
--swiftversion 5.9
--extensionacl on-declarations
43 changes: 39 additions & 4 deletions Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,51 @@ enum CheckoutBridge: CheckoutBridgeProtocol {
}

static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?) {
let dispatchMessageBody: String
if let body = messageBody {
dispatchMessageBody = "'\(messageName)', \(body)"
let dispatchMessageBody = if let body = messageBody {
"'\(messageName)', \(body)"
} else {
dispatchMessageBody = "'\(messageName)'"
"'\(messageName)'"
}
let script = dispatchMessageTemplate(body: dispatchMessageBody)
webView.evaluateJavaScript(script)
}

/// Build embed query parameter string for embedded checkout
static func embedParams(authToken: String? = nil) -> String {
var params: [String] = []

// Authentication token (required for embedded checkout)
if let token = authToken {
params.append("authentication=\(token)")
}

// Protocol version (always 2025-04 for embedded checkout)
params.append("protocol=2025-04")

// Color scheme
let colorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
let normalizedColorScheme: String
switch colorScheme {
case .automatic:
normalizedColorScheme = "automatic"
case .light:
normalizedColorScheme = "light"
case .dark:
normalizedColorScheme = "dark"
case .web:
normalizedColorScheme = "web"
}
params.append("color-scheme=\(normalizedColorScheme)")

// Library identification
params.append("library=CheckoutKit/4.0.0")

// Platform identification (valid: ReactNative, Swift, Android, Web)
params.append("platform=Swift")

return params.joined(separator: ",")
}

static func decode(_ message: WKScriptMessage) throws -> WebEvent {
guard let body = message.body as? String, let data = body.data(using: .utf8) else {
throw BridgeError.invalidBridgeEvent()
Expand Down
32 changes: 32 additions & 0 deletions Sources/ShopifyCheckoutSheetKit/CheckoutDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@

/// Tells te delegate that a Web Pixel event was emitted
func checkoutDidEmitWebPixelEvent(event: PixelEvent)

Check warning on line 47 in Sources/ShopifyCheckoutSheetKit/CheckoutDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// MARK: - Embedded Checkout Events (Schema 2025-04)

/// Tells the delegate that the embedded checkout state has changed
func checkoutDidChangeState(state: CheckoutStatePayload)

/// Tells the delegate that the embedded checkout successfully completed with order details
func checkoutDidComplete(payload: CheckoutCompletePayload)

Check warning on line 55 in Sources/ShopifyCheckoutSheetKit/CheckoutDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
/// Tells the delegate that the embedded checkout encountered one or more errors
func checkoutDidFail(errors: [ErrorPayload])

/// Tells the delegate that an embedded checkout web pixel event was emitted
func checkoutDidEmitWebPixelEvent(payload: WebPixelsPayload)
}

extension CheckoutDelegate {
Expand All @@ -68,4 +82,22 @@
UIApplication.shared.open(url)
}
}

// MARK: - Embedded Checkout Default Implementations

Check warning on line 87 in Sources/ShopifyCheckoutSheetKit/CheckoutDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
public func checkoutDidChangeState(state: CheckoutStatePayload) {
/// No-op by default
}

public func checkoutDidComplete(payload: CheckoutCompletePayload) {
/// No-op by default
}

public func checkoutDidFail(errors: [ErrorPayload]) {
/// No-op by default
}

Check warning on line 99 in Sources/ShopifyCheckoutSheetKit/CheckoutDelegate.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
public func checkoutDidEmitWebPixelEvent(payload: WebPixelsPayload) {
/// No-op by default
}
}
179 changes: 175 additions & 4 deletions Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import Common
import UIKit
import WebKit

Check warning on line 26 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'WebKit'

Check warning on line 26 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'WebKit'

protocol CheckoutWebViewDelegate: AnyObject {
func checkoutViewDidStartNavigation()
Expand All @@ -33,6 +33,12 @@
func checkoutViewDidFailWithError(error: CheckoutError)
func checkoutViewDidToggleModal(modalVisible: Bool)
func checkoutViewDidEmitWebPixelEvent(event: PixelEvent)

Check failure on line 36 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// MARK: - Embedded Checkout Events (Schema 2025-04)
func checkoutViewDidChangeState(state: CheckoutStatePayload)
func checkoutViewDidComplete(payload: CheckoutCompletePayload)
func checkoutViewDidFail(errors: [ErrorPayload])
func checkoutViewDidEmitWebPixelEvent(payload: WebPixelsPayload)
}

private let deprecatedReasonHeader = "x-shopify-api-deprecated-reason"
Expand Down Expand Up @@ -130,6 +136,8 @@
var isPreloadRequest: Bool = false

private var entryPoint: MetaData.EntryPoint?

Check failure on line 139 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
var checkoutOptions: CheckoutOptions?

// MARK: Initializers

Expand Down Expand Up @@ -232,7 +240,10 @@

func load(checkout url: URL, isPreload: Bool = false) {
OSLogger.shared.info("Loading checkout URL: \(url.absoluteString), isPreload: \(isPreload)")
var request = URLRequest(url: url)

Check failure on line 243 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// Build URL with embed query parameter if needed
let finalURL = buildCheckoutURL(from: url, options: checkoutOptions)
var request = URLRequest(url: finalURL)

if isPreload, isPreloadingAvailable {
isPreloadRequest = true
Expand All @@ -241,6 +252,33 @@

load(request)
}

Check failure on line 255 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
private func buildCheckoutURL(from originalURL: URL, options: CheckoutOptions?) -> URL {
// Get authentication token if provided
var authToken: String?
if let appAuth = options?.appAuthentication {
switch appAuth {
case .token(let token):
authToken = token
}
}

Check failure on line 265 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// Only add embed parameter if we have options (indicating embedded checkout)
guard options != nil else {
return originalURL
}

// Get embed params string with authentication token
let embedValue = CheckoutBridge.embedParams(authToken: authToken)

// Add the embed parameter to the URL
var components = URLComponents(url: originalURL, resolvingAgainstBaseURL: false)
var queryItems = components?.queryItems ?? []
queryItems.append(URLQueryItem(name: "embed", value: embedValue))
components?.queryItems = queryItems

return components?.url ?? originalURL
}

private func dispatchPresentedMessage(_ checkoutDidLoad: Bool, _ checkoutDidPresent: Bool) {
if checkoutDidLoad, checkoutDidPresent, isBridgeAttached {
Expand All @@ -252,12 +290,53 @@
}

extension CheckoutWebView: WKScriptMessageHandler {
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {

Check failure on line 293 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Cyclomatic Complexity Violation: Function should have complexity 10 or less; currently complexity is 20 (cyclomatic_complexity)

Check warning on line 293 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

Cyclomatic Complexity Violation: Function should have complexity 10 or less; currently complexity is 20 (cyclomatic_complexity)
guard let body = message.body as? String else {
OSLogger.shared.error("Invalid script message body")
return
}

// Try to decode as JSON first for embedded checkout events
if let eventData = try? JSONSerialization.jsonObject(with: Data(body.utf8), options: []) as? [String: Any],
let eventName = eventData["name"] as? String {

Check failure on line 302 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
switch eventName {
case "stateChange":
OSLogger.shared.info("Embedded checkout state change event received")
handleStateChangeEvent(eventData)
return
case "completed":
OSLogger.shared.info("Embedded checkout completed event received")

Check failure on line 309 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
if handleEmbeddedCompletedEvent(eventData) {
return
}
// Fall through to legacy handling if embedded handling fails
case "error":
OSLogger.shared.error("Embedded checkout error event received")
if handleEmbeddedErrorEvent(eventData) {
return
}
// Fall through to legacy handling if embedded handling fails
case "webPixels":
OSLogger.shared.info("Embedded checkout web pixels event received")
handleEmbeddedWebPixelEvent(eventData)
return
case "checkoutBlockingEvent":
if let modalVisible = eventData["body"] as? String, let visible = Bool(modalVisible) {
viewDelegate?.checkoutViewDidToggleModal(modalVisible: visible)
}
return
default:
break
}
}

Check failure on line 333 in Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// Fall back to legacy CheckoutBridge decoding for existing events
do {
switch try CheckoutBridge.decode(message) {
/// Completed event
/// Completed event
case let .checkoutComplete(checkoutCompletedEvent):
OSLogger.shared.info("Checkout completed event received")
OSLogger.shared.info("Legacy checkout completed event received")
viewDelegate?.checkoutViewDidCompleteCheckout(event: checkoutCompletedEvent)
/// Error: Checkout unavailable
case let .checkoutUnavailable(message, code):
Expand Down Expand Up @@ -290,13 +369,105 @@
viewDelegate?.checkoutViewDidEmitWebPixelEvent(event: nonOptionalEvent)
}
default:
()
OSLogger.shared.debug("Unsupported legacy event")
}
} catch {
OSLogger.shared.error("Error decoding bridge script message: \(error.localizedDescription)")
viewDelegate?.checkoutViewDidFailWithError(error: .sdkError(underlying: error))
}
}

// MARK: - Embedded Checkout Event Handlers

private func handleStateChangeEvent(_ event: [String: Any]) {
guard let statePayload = decodeStateChangePayload(from: event) else {
OSLogger.shared.error("Failed to decode state change payload")
return
}
viewDelegate?.checkoutViewDidChangeState(state: statePayload)
}

private func handleEmbeddedCompletedEvent(_ event: [String: Any]) -> Bool {
guard let embeddedPayload = decodeEmbeddedCompletePayload(from: event) else {
return false
}
viewDelegate?.checkoutViewDidComplete(payload: embeddedPayload)
return true
}

private func handleEmbeddedErrorEvent(_ event: [String: Any]) -> Bool {
guard let embeddedErrors = decodeEmbeddedErrorPayload(from: event) else {
return false
}
viewDelegate?.checkoutViewDidFail(errors: embeddedErrors)
return true
}

private func handleEmbeddedWebPixelEvent(_ event: [String: Any]) {
guard let embeddedPayload = decodeEmbeddedWebPixelPayload(from: event) else {
OSLogger.shared.error("Failed to decode embedded checkout web pixel payload")
return
}
viewDelegate?.checkoutViewDidEmitWebPixelEvent(payload: embeddedPayload)
}

// MARK: - Embedded Checkout Decoders

private func decodeEmbeddedCompletePayload(from event: [String: Any]) -> CheckoutCompletePayload? {
guard let messageBody = event["body"] as? String,
let data = messageBody.data(using: .utf8) else {
return nil
}

do {
return try JSONDecoder().decode(CheckoutCompletePayload.self, from: data)
} catch {
OSLogger.shared.debug("Failed to decode as embedded complete payload: \(error)")
return nil
}
}

private func decodeEmbeddedWebPixelPayload(from event: [String: Any]) -> WebPixelsPayload? {
guard let messageBody = event["body"] as? String,
let data = messageBody.data(using: .utf8) else {
return nil
}

do {
return try JSONDecoder().decode(WebPixelsPayload.self, from: data)
} catch {
OSLogger.shared.debug("Failed to decode as embedded web pixel payload: \(error)")
return nil
}
}

private func decodeStateChangePayload(from event: [String: Any]) -> CheckoutStatePayload? {
guard let messageBody = event["body"] as? String,
let data = messageBody.data(using: .utf8) else {
return nil
}

do {
return try JSONDecoder().decode(CheckoutStatePayload.self, from: data)
} catch {
OSLogger.shared.error("Failed to decode state change payload: \(error)")
return nil
}
}

private func decodeEmbeddedErrorPayload(from event: [String: Any]) -> [ErrorPayload]? {
guard let messageBody = event["body"] as? String,
let data = messageBody.data(using: .utf8) else {
return nil
}

do {
return try JSONDecoder().decode([ErrorPayload].self, from: data)
} catch {
OSLogger.shared.debug("Failed to decode as embedded error payload: \(error)")
return nil
}
}
}

extension CheckoutWebView: WKNavigationDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

// MARK: Initializers

public init(checkoutURL url: URL, delegate: CheckoutDelegate? = nil, entryPoint: MetaData.EntryPoint? = nil) {
public init(checkoutURL url: URL, delegate: CheckoutDelegate? = nil, entryPoint: MetaData.EntryPoint? = nil, options: CheckoutOptions? = nil) {
checkoutURL = url
self.delegate = delegate

Expand All @@ -71,6 +71,9 @@
checkoutView.scrollView.contentInsetAdjustmentBehavior = .never
self.checkoutView = checkoutView

// Set checkout options for embedded checkout support
checkoutView.checkoutOptions = options

super.init(nibName: nil, bundle: nil)

title = ShopifyCheckoutSheetKit.configuration.title
Expand Down Expand Up @@ -246,6 +249,39 @@
func checkoutViewDidEmitWebPixelEvent(event: PixelEvent) {
delegate?.checkoutDidEmitWebPixelEvent(event: event)
}

Check warning on line 252 in Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift

View workflow job for this annotation

GitHub Actions / call-workflow-passing-data / test

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
// MARK: - Embedded Checkout Delegate Methods

func checkoutViewDidChangeState(state: CheckoutStatePayload) {
delegate?.checkoutDidChangeState(state: state)
}

func checkoutViewDidComplete(payload: CheckoutCompletePayload) {
ConfettiCannon.fire(in: view)
CheckoutWebView.invalidate(disconnect: false)
delegate?.checkoutDidComplete(payload: payload)
}

func checkoutViewDidFail(errors: [ErrorPayload]) {
CheckoutWebView.invalidate()
delegate?.checkoutDidFail(errors: errors)

// For embedded checkout errors, attempt recovery based on error group and URL type
let shouldAttemptRecovery = errors.contains { error in
// Only recover from specific recoverable network errors (5xx server errors)
return error.group == "network" && error.type == "server_error" && (error.code?.hasPrefix("5") == true)
} && !CheckoutURL(from: checkoutURL).isMultipassURL()

if shouldAttemptRecovery {
presentFallbackViewController(url: checkoutURL)
} else {
dismiss(animated: true)
}
}

func checkoutViewDidEmitWebPixelEvent(payload: WebPixelsPayload) {
delegate?.checkoutDidEmitWebPixelEvent(payload: payload)
}

private func canRecoverFromError(_: CheckoutError) -> Bool {
/// Reuse of multipass tokens will cause 422 errors. A new token must be generated
Expand Down
Loading
Loading