From d2bc6d32c05b38284884c02b9f11331ca987397e Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Tue, 29 Sep 2026 09:40:59 +0100 Subject: [PATCH 1/4] Allow Swift hosts to handle navigation links --- .../ShopifyCheckoutKit/CheckoutWebView.swift | 17 ++++++--- .../CheckoutWebViewTests.swift | 37 +++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift index a6afd0f64..49f316b89 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift @@ -860,11 +860,18 @@ extension CheckoutWebView: WKNavigationDelegate { // - Deep links on offsite payment sites // if CheckoutURL(from: url).isDeepLink() { - Task { @MainActor [externalURLHandler] in - if await externalURLHandler.open(url) { - OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - opened externally") - } else { - OSLogger.shared.error("Deep link rejected: \(LogSafeURL.string(url)). No installed app can open this scheme.") + Task { @MainActor [externalURLHandler, linkActionProvider] in + switch linkActionProvider?(CheckoutLink(url: url)) ?? .open { + case .handled: + OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - handled") + case .cancel: + OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - canceled") + case .open: + if await externalURLHandler.open(url) { + OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - opened externally") + } else { + OSLogger.shared.error("Deep link rejected: \(LogSafeURL.string(url)). No installed app can open this scheme.") + } } decisionHandler(.cancel) } diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift index baaa07a9a..9731c1d73 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift @@ -126,6 +126,43 @@ class CheckoutWebViewTests: XCTestCase { XCTAssertEqual(handler.openedURL, link, "The deep link should be opened via the external URL handler exactly once") } + func testDeepLinkHandledByLinkActionProviderIsNotOpenedExternally() throws { + let link = try XCTUnwrap(URL(string: "myapp://checkout")) + let handler = MockExternalURLHandler(didOpen: true) + view.externalURLHandler = handler + var handledLink: CheckoutLink? + view.linkActionProvider = { checkoutLink in + handledLink = checkoutLink + return .handled + } + let received = expectation(description: "policy decided") + + view.webView(view, decidePolicyFor: MockExternalNavigationAction(url: link)) { policy in + XCTAssertEqual(policy, .cancel) + received.fulfill() + } + + wait(for: [received], timeout: 2.0) + XCTAssertEqual(handledLink?.url, link) + XCTAssertNil(handler.openedURL) + } + + func testDeepLinkCanceledByLinkActionProviderIsNotOpenedExternally() throws { + let link = try XCTUnwrap(URL(string: "myapp://checkout")) + let handler = MockExternalURLHandler(didOpen: true) + view.externalURLHandler = handler + view.linkActionProvider = { _ in .cancel } + let received = expectation(description: "policy decided") + + view.webView(view, decidePolicyFor: MockExternalNavigationAction(url: link)) { policy in + XCTAssertEqual(policy, .cancel) + received.fulfill() + } + + wait(for: [received], timeout: 2.0) + XCTAssertNil(handler.openedURL) + } + func testHTTPSubframeRequestIsAllowed() throws { let link = try XCTUnwrap(URL(string: "https://shopify1.shopify.com/checkouts/cn/123")) let received = expectation(description: "policy decided") From d67831a10d3ae5f036645a5b8aa7158d1454f530 Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Tue, 29 Sep 2026 10:48:46 +0100 Subject: [PATCH 2/4] Clarify Swift link handling logs --- .../Sources/ShopifyCheckoutKit/CheckoutWebView.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift index 49f316b89..e25168e88 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift @@ -860,17 +860,19 @@ extension CheckoutWebView: WKNavigationDelegate { // - Deep links on offsite payment sites // if CheckoutURL(from: url).isDeepLink() { + OSLogger.shared.debug("Raw navigation link intercepted: \(LogSafeURL.string(url))") Task { @MainActor [externalURLHandler, linkActionProvider] in switch linkActionProvider?(CheckoutLink(url: url)) ?? .open { case .handled: - OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - handled") + OSLogger.shared.debug("Raw navigation link handled by host") case .cancel: - OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - canceled") + OSLogger.shared.debug("Raw navigation link canceled by host") case .open: + OSLogger.shared.debug("Raw navigation link delegated to SDK default handler") if await externalURLHandler.open(url) { - OSLogger.shared.debug("Deep link intercepted: \(LogSafeURL.string(url)) - opened externally") + OSLogger.shared.debug("Raw navigation link opened with SDK default handler") } else { - OSLogger.shared.error("Deep link rejected: \(LogSafeURL.string(url)). No installed app can open this scheme.") + OSLogger.shared.error("Raw navigation link rejected by SDK default handler") } } decisionHandler(.cancel) From 908b0a20109e8943361131bb7c7323ae0db2c943 Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Wed, 30 Sep 2026 13:12:37 +0100 Subject: [PATCH 3/4] Log Swift sample checkout link handling --- .../CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift index 151e6fae5..c3c2e9cf2 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift @@ -204,10 +204,13 @@ struct CartView: View { } private func handleCheckoutLink(_ link: CheckoutLink) -> CheckoutLinkAction { + print("[CheckoutKitSwiftDemo] Checkout link clicked: \(link.url)") switch windowOpenHandler { case .default: + print("[CheckoutKitSwiftDemo] Checkout link delegated to Checkout Kit: \(link.url)") return .open case .externalApp: + print("[CheckoutKitSwiftDemo] Checkout link handled by sample: \(link.url)") UIApplication.shared.open(link.url) return .handled } From f40a3e337d00292c030b96ddee3bcb43430d67e6 Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Thu, 1 Oct 2026 15:31:59 +0100 Subject: [PATCH 4/4] Trim redundant Swift navigation logs --- .../swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift index e25168e88..0dd093bb2 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift @@ -860,7 +860,6 @@ extension CheckoutWebView: WKNavigationDelegate { // - Deep links on offsite payment sites // if CheckoutURL(from: url).isDeepLink() { - OSLogger.shared.debug("Raw navigation link intercepted: \(LogSafeURL.string(url))") Task { @MainActor [externalURLHandler, linkActionProvider] in switch linkActionProvider?(CheckoutLink(url: url)) ?? .open { case .handled: @@ -868,7 +867,6 @@ extension CheckoutWebView: WKNavigationDelegate { case .cancel: OSLogger.shared.debug("Raw navigation link canceled by host") case .open: - OSLogger.shared.debug("Raw navigation link delegated to SDK default handler") if await externalURLHandler.open(url) { OSLogger.shared.debug("Raw navigation link opened with SDK default handler") } else {