From 4e422baf26265cc79debd76b7fb724fcbdb2f85b Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Tue, 2 Jun 2026 18:01:36 +0100 Subject: [PATCH] Await checkout bridge JavaScript evaluation --- .../ShopifyCheckoutKit/CheckoutBridge.swift | 16 +++++++++--- .../ShopifyCheckoutKit/CheckoutWebView.swift | 16 +++++++++--- .../CheckoutBridgeTests.swift | 26 +++++++------------ .../CheckoutWebViewTests.swift | 3 ++- .../Mocks/MockWebView.swift | 9 ++----- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift index 65e6401b..0db45714 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift @@ -7,7 +7,7 @@ enum BridgeError: Swift.Error { protocol CheckoutBridgeProtocol { static func instrument(_ webView: WKWebView, _ instrumentation: InstrumentationPayload) - static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?) + @discardableResult static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?) async -> Bool static func sendResponse(_ webView: WKWebView, messageBody: String) } @@ -27,11 +27,13 @@ enum CheckoutBridge: CheckoutBridgeProtocol { static func instrument(_ webView: WKWebView, _ instrumentation: InstrumentationPayload) { if let payload = instrumentation.toBridgeEvent() { - sendMessage(webView, messageName: "instrumentation", messageBody: payload) + Task { + await sendMessage(webView, messageName: "instrumentation", messageBody: payload) + } } } - static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?) { + @discardableResult static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?) async -> Bool { let dispatchMessageBody: String if let body = messageBody { dispatchMessageBody = "'\(messageName)', \(body)" @@ -39,7 +41,13 @@ enum CheckoutBridge: CheckoutBridgeProtocol { dispatchMessageBody = "'\(messageName)'" } let script = dispatchMessageTemplate(body: dispatchMessageBody) - webView.evaluateJavaScript(script) + do { + try await webView.evaluateJavaScript(script) + return true + } catch { + OSLogger.shared.error("Failed to dispatch bridge message: \(error.localizedDescription)") + return false + } } static func sendResponse(_ webView: WKWebView, messageBody: String) { diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift index 5b6dae68..7bf2216e 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift @@ -66,6 +66,7 @@ class CheckoutWebView: WKWebView { weak var viewDelegate: CheckoutWebViewDelegate? var presentedEventDidDispatch = false + private var presentedEventDispatchInFlight = false var checkoutDidPresent: Bool = false { didSet { dispatchPresentedMessage(checkoutDidLoad, checkoutDidPresent) @@ -151,10 +152,17 @@ class CheckoutWebView: WKWebView { } private func dispatchPresentedMessage(_ checkoutDidLoad: Bool, _ checkoutDidPresent: Bool) { - if checkoutDidLoad, checkoutDidPresent, isBridgeAttached { - OSLogger.shared.info("Emitting presented event to checkout") - CheckoutBridge.sendMessage(self, messageName: "presented", messageBody: nil) - presentedEventDidDispatch = true + guard checkoutDidLoad, checkoutDidPresent, isBridgeAttached, !presentedEventDidDispatch, !presentedEventDispatchInFlight else { + return + } + + OSLogger.shared.info("Emitting presented event to checkout") + presentedEventDispatchInFlight = true + Task { @MainActor [weak self] in + guard let self else { return } + let didDispatch = await CheckoutBridge.sendMessage(self, messageName: "presented", messageBody: nil) + presentedEventDispatchInFlight = false + presentedEventDidDispatch = didDispatch } } } diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutBridgeTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutBridgeTests.swift index 7e020078..dc8470b9 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutBridgeTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutBridgeTests.swift @@ -54,30 +54,24 @@ class CheckoutBridgeTests: XCTestCase { } } - func testSendMessageShouldCallEvaluateJavaScriptPresented() { + @MainActor + func testSendMessageShouldCallEvaluateJavaScriptPresented() async { let webView = MockWebView() - webView.expectedScript = expectedPresentedScript() - let evaluateJavaScriptExpectation = expectation( - description: "evaluateJavaScript was called" - ) - webView.evaluateJavaScriptExpectation = evaluateJavaScriptExpectation - CheckoutBridge.sendMessage(webView, messageName: "presented", messageBody: nil) + let didDispatch = await CheckoutBridge.sendMessage(webView, messageName: "presented", messageBody: nil) - wait(for: [evaluateJavaScriptExpectation], timeout: 2) + XCTAssertTrue(didDispatch) + XCTAssertEqual(webView.evaluatedScript, expectedPresentedScript()) } - func testSendMessageWithPayloadEvaulatesJavaScript() { + @MainActor + func testSendMessageWithPayloadEvaulatesJavaScript() async { let webView = MockWebView() - webView.expectedScript = expectedPayloadScript() - let evaluateJavaScriptExpectation = expectation( - description: "evaluateJavaScript was called" - ) - webView.evaluateJavaScriptExpectation = evaluateJavaScriptExpectation - CheckoutBridge.sendMessage(webView, messageName: "payload", messageBody: "{\"one\": true}") + let didDispatch = await CheckoutBridge.sendMessage(webView, messageName: "payload", messageBody: "{\"one\": true}") - wait(for: [evaluateJavaScriptExpectation], timeout: 2) + XCTAssertTrue(didDispatch) + XCTAssertEqual(webView.evaluatedScript, expectedPayloadScript()) } private func expectedPresentedScript() -> String { diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift index 60fc5989..2f1f9f12 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift @@ -536,8 +536,9 @@ class MockCheckoutBridge: CheckoutBridgeProtocol { instrumentCalled = true } - static func sendMessage(_: WKWebView, messageName _: String, messageBody _: String?) { + static func sendMessage(_: WKWebView, messageName _: String, messageBody _: String?) async -> Bool { sendMessageCalled = true + return true } static func sendResponse(_: WKWebView, messageBody: String) { diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockWebView.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockWebView.swift index d7a4e014..9cb44a14 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockWebView.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockWebView.swift @@ -1,16 +1,11 @@ @testable import ShopifyCheckoutKit import WebKit -import XCTest class MockWebView: CheckoutWebView { - var expectedScript = "" - - var evaluateJavaScriptExpectation: XCTestExpectation? + var evaluatedScript: String? override func evaluateJavaScript(_ javaScriptString: String) async throws -> Any { - if javaScriptString == expectedScript { - evaluateJavaScriptExpectation?.fulfill() - } + evaluatedScript = javaScriptString return true } }