Skip to content

Commit bca9980

Browse files
committed
work in progress changes to migrate onto checkout sheet protocol
1 parent 9c36011 commit bca9980

File tree

7 files changed

+377
-33
lines changed

7 files changed

+377
-33
lines changed

Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,30 +34,19 @@ protocol CheckoutBridgeProtocol {
3434
}
3535

3636
enum CheckoutBridge: CheckoutBridgeProtocol {
37-
static let schemaVersion = "8.1"
37+
static let schemaVersion = "2025-04"
3838
static let messageHandler = "mobileCheckoutSdk"
39-
internal static let userAgent = "ShopifyCheckoutSDK/\(ShopifyCheckoutSheetKit.version)"
4039

4140
static var applicationName: String {
42-
let theme = ShopifyCheckoutSheetKit.configuration.colorScheme.rawValue
43-
let userAgentString = "\(userAgent) (\(schemaVersion);\(theme);standard)"
44-
45-
return userAgentWithOptionalSuffix(userAgentString)
41+
let platform = ShopifyCheckoutSheetKit.configuration.platform?.rawValue ?? "iOS"
42+
let userAgentString = "CheckoutKit/\(ShopifyCheckoutSheetKit.version) (\(platform)) CheckoutSheetProtocol/\(schemaVersion)"
43+
return userAgentString
4644
}
4745

4846
static var recoveryAgent: String {
49-
let theme = ShopifyCheckoutSheetKit.configuration.colorScheme.rawValue
50-
let userAgentString = "\(userAgent) (noconnect;\(theme);standard_recovery)"
51-
52-
return userAgentWithOptionalSuffix(userAgentString)
53-
}
54-
55-
static func userAgentWithOptionalSuffix(_ userAgentString: String) -> String {
56-
if let platform = ShopifyCheckoutSheetKit.configuration.platform?.rawValue {
57-
return "\(userAgentString) \(platform)"
58-
} else {
59-
return userAgentString
60-
}
47+
let platform = ShopifyCheckoutSheetKit.configuration.platform?.rawValue ?? "iOS"
48+
let userAgentString = "CheckoutKit/\(ShopifyCheckoutSheetKit.version) (\(platform)) CheckoutSheetProtocol/noconnect"
49+
return userAgentString
6150
}
6251

6352
static func instrument(_ webView: WKWebView, _ instrumentation: InstrumentationPayload) {
@@ -91,11 +80,11 @@ enum CheckoutBridge: CheckoutBridgeProtocol {
9180

9281
static internal func dispatchMessageTemplate(body: String) -> String {
9382
return """
94-
if (window.MobileCheckoutSdk && window.MobileCheckoutSdk.dispatchMessage) {
95-
window.MobileCheckoutSdk.dispatchMessage(\(body));
83+
if (window.Shopify?.CheckoutSheetProtocol?.postMessage) {
84+
window.Shopify.CheckoutSheetProtocol.postMessage(\(body));
9685
} else {
9786
window.addEventListener('mobileCheckoutBridgeReady', function () {
98-
window.MobileCheckoutSdk.dispatchMessage(\(body));
87+
window.Shopify.CheckoutSheetProtocol.postMessage(\(body));
9988
}, {passive: true, once: true});
10089
}
10190
"""

Sources/ShopifyCheckoutSheetKit/CheckoutURL.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,12 @@ public struct CheckoutURL {
5959

6060
return !["http", "https"].contains(scheme)
6161
}
62+
63+
public func isWebLink() -> Bool {
64+
guard let scheme = url.scheme else {
65+
return false
66+
}
67+
68+
return ["http", "https"].contains(scheme)
69+
}
6270
}

Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,12 @@ class CheckoutWebView: WKWebView {
228228

229229
if isPreload && isPreloadingAvailable {
230230
isPreloadRequest = true
231-
request.setValue("prefetch", forHTTPHeaderField: "Sec-Purpose")
231+
}
232+
233+
// Add checkout kit headers
234+
let headers = checkoutKitHeaders(isPreload: isPreload)
235+
for (key, value) in headers {
236+
request.setValue(value, forHTTPHeaderField: key)
232237
}
233238

234239
load(request)
@@ -305,6 +310,39 @@ extension CheckoutWebView: WKNavigationDelegate {
305310
return
306311
}
307312

313+
// Check if we need to inject headers for main frame navigation
314+
if action.targetFrame?.isMainFrame == true && CheckoutURL(from: url).isWebLink() {
315+
let currentHeaders = action.request.allHTTPHeaderFields ?? [:]
316+
let checkoutHeaders = checkoutKitHeaders()
317+
var shouldOverride = false
318+
var newHeaders = currentHeaders
319+
320+
let colorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
321+
let shouldHaveColorSchemeHeader = !(colorScheme == .web || colorScheme == .automatic)
322+
323+
if shouldHaveColorSchemeHeader && !currentHeaders.hasColorSchemeHeader() {
324+
let headersWithColorScheme = currentHeaders.withColorScheme()
325+
newHeaders.merge(headersWithColorScheme) { _, new in new }
326+
shouldOverride = true
327+
}
328+
329+
if !currentHeaders.hasBrandingHeader() {
330+
let headersWithBranding = currentHeaders.withBranding()
331+
newHeaders.merge(headersWithBranding) { _, new in new }
332+
shouldOverride = true
333+
}
334+
335+
if shouldOverride {
336+
var request = URLRequest(url: url)
337+
for (key, value) in newHeaders {
338+
request.setValue(value, forHTTPHeaderField: key)
339+
}
340+
webView.load(request)
341+
decisionHandler(.cancel)
342+
return
343+
}
344+
}
345+
308346
decisionHandler(.allow)
309347
}
310348

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
MIT License
3+
4+
Copyright 2023 - Present, Shopify Inc.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
24+
import Foundation
25+
26+
internal struct Headers {
27+
static let purpose = "Sec-Purpose"
28+
static let purposePrefetch = "prefetch"
29+
30+
static let prefersColorScheme = "Sec-CH-Prefers-Color-Scheme"
31+
32+
static let branding = "X-Shopify-Checkout-Kit-Branding"
33+
static let brandingCheckoutKit = "CHECKOUT_KIT"
34+
static let brandingWeb = "WEB_DEFAULT"
35+
}
36+
37+
internal func checkoutKitHeaders(isPreload: Bool = false) -> [String: String] {
38+
var headers = [String: String]()
39+
40+
if isPreload {
41+
headers[Headers.purpose] = Headers.purposePrefetch
42+
}
43+
44+
return headers
45+
.withColorScheme()
46+
.withBranding()
47+
}
48+
49+
internal extension Dictionary where Key == String, Value == String {
50+
func withColorScheme() -> [String: String] {
51+
var headers = self
52+
53+
let colorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
54+
switch colorScheme {
55+
case .light:
56+
headers[Headers.prefersColorScheme] = "light"
57+
case .dark:
58+
headers[Headers.prefersColorScheme] = "dark"
59+
case .automatic, .web:
60+
break // Don't add header for automatic or web color schemes
61+
}
62+
63+
return headers
64+
}
65+
66+
func withBranding() -> [String: String] {
67+
var headers = self
68+
69+
let colorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
70+
switch colorScheme {
71+
case .web:
72+
headers[Headers.branding] = Headers.brandingWeb
73+
default:
74+
headers[Headers.branding] = Headers.brandingCheckoutKit
75+
}
76+
77+
return headers
78+
}
79+
80+
func hasColorSchemeHeader() -> Bool {
81+
return hasHeader(Headers.prefersColorScheme)
82+
}
83+
84+
func hasBrandingHeader() -> Bool {
85+
return hasHeader(Headers.branding)
86+
}
87+
88+
private func hasHeader(_ headerName: String) -> Bool {
89+
return self.keys.contains { key in
90+
key.caseInsensitiveCompare(headerName) == .orderedSame
91+
}
92+
}
93+
}

Tests/ShopifyCheckoutSheetKitTests/CheckoutBridgeTests.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,20 @@ class CheckoutBridgeTests: XCTestCase {
4242
func testReturnsStandardUserAgent() {
4343
let version = ShopifyCheckoutSheetKit.version
4444
let schemaVersion = CheckoutBridge.schemaVersion
45-
XCTAssertEqual(CheckoutBridge.applicationName, "ShopifyCheckoutSDK/\(version) (\(schemaVersion);automatic;standard)")
45+
XCTAssertEqual(CheckoutBridge.applicationName, "CheckoutKit/\(version) (iOS) CheckoutSheetProtocol/\(schemaVersion)")
4646
}
4747

4848
func testReturnsRecoveryUserAgent() {
4949
let version = ShopifyCheckoutSheetKit.version
50-
XCTAssertEqual(CheckoutBridge.recoveryAgent, "ShopifyCheckoutSDK/\(version) (noconnect;automatic;standard_recovery)")
50+
XCTAssertEqual(CheckoutBridge.recoveryAgent, "CheckoutKit/\(version) (iOS) CheckoutSheetProtocol/noconnect")
5151
}
5252

5353
func testReturnsUserAgentWithCustomPlatformSuffix() {
5454
let version = ShopifyCheckoutSheetKit.version
5555
let schemaVersion = CheckoutBridge.schemaVersion
5656
ShopifyCheckoutSheetKit.configuration.platform = Platform.reactNative
57-
XCTAssertEqual(CheckoutBridge.applicationName, "ShopifyCheckoutSDK/\(version) (\(schemaVersion);automatic;standard) ReactNative")
58-
XCTAssertEqual(CheckoutBridge.recoveryAgent, "ShopifyCheckoutSDK/\(version) (noconnect;automatic;standard_recovery) ReactNative")
57+
XCTAssertEqual(CheckoutBridge.applicationName, "CheckoutKit/\(version) (ReactNative) CheckoutSheetProtocol/\(schemaVersion)")
58+
XCTAssertEqual(CheckoutBridge.recoveryAgent, "CheckoutKit/\(version) (ReactNative) CheckoutSheetProtocol/noconnect")
5959
ShopifyCheckoutSheetKit.configuration.platform = nil
6060
}
6161

@@ -320,23 +320,23 @@ class CheckoutBridgeTests: XCTestCase {
320320

321321
private func expectedPresentedScript() -> String {
322322
return """
323-
if (window.MobileCheckoutSdk && window.MobileCheckoutSdk.dispatchMessage) {
324-
window.MobileCheckoutSdk.dispatchMessage('presented');
323+
if (window.Shopify?.CheckoutSheetProtocol?.postMessage) {
324+
window.Shopify.CheckoutSheetProtocol.postMessage('presented');
325325
} else {
326326
window.addEventListener('mobileCheckoutBridgeReady', function () {
327-
window.MobileCheckoutSdk.dispatchMessage('presented');
327+
window.Shopify.CheckoutSheetProtocol.postMessage('presented');
328328
}, {passive: true, once: true});
329329
}
330330
"""
331331
}
332332

333333
private func expectedPayloadScript() -> String {
334334
return """
335-
if (window.MobileCheckoutSdk && window.MobileCheckoutSdk.dispatchMessage) {
336-
window.MobileCheckoutSdk.dispatchMessage('payload', {"one": true});
335+
if (window.Shopify?.CheckoutSheetProtocol?.postMessage) {
336+
window.Shopify.CheckoutSheetProtocol.postMessage('payload', {"one": true});
337337
} else {
338338
window.addEventListener('mobileCheckoutBridgeReady', function () {
339-
window.MobileCheckoutSdk.dispatchMessage('payload', {"one": true});
339+
window.Shopify.CheckoutSheetProtocol.postMessage('payload', {"one": true});
340340
}, {passive: true, once: true});
341341
}
342342
"""

Tests/ShopifyCheckoutSheetKitTests/CheckoutWebViewTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class CheckoutWebViewTests: XCTestCase {
6161
XCTAssertTrue(recovery.isRecovery)
6262
XCTAssertFalse(recovery.isBridgeAttached)
6363
XCTAssertFalse(recovery.isPreloadingAvailable)
64-
XCTAssertEqual(recovery.configuration.applicationNameForUserAgent, "ShopifyCheckoutSDK/\(ShopifyCheckoutSheetKit.version) (noconnect;automatic;standard_recovery)")
64+
XCTAssertEqual(recovery.configuration.applicationNameForUserAgent, "CheckoutKit/\(ShopifyCheckoutSheetKit.version) (iOS) CheckoutSheetProtocol/noconnect")
6565
XCTAssertTrue(recovery.configuration.allowsInlineMediaPlayback)
6666
XCTAssertEqual(recovery.backgroundColor, backgroundColor)
6767
XCTAssertFalse(recovery.isOpaque)

0 commit comments

Comments
 (0)