Skip to content

Commit b4b9714

Browse files
[AC][CSK]: Add UserAgent to AC requests, update CSK UserAgent **only** when launched from AC (#352)
* feat: introduce Common product for UserAgent * refactor: migrate ShopifyCheckoutSheetKit to use Common dir * feat: setup user agent for ShopifyAcceleratedCheckouts * refactor: use package access visibility for common dir * fix: move entrypoint parameter to end of UA - remove label * fix: agent not attaching due to configuration not modifying presenting view * fix: pod lint issues * chore: update comment for internal present function * refactor: move constants to MetaData enum * fix: pod spec failure on CI * fix: test failure from entrypoint appearing on cached requests * refactor: make MetaData package scoped
1 parent 9b0f0f0 commit b4b9714

16 files changed

+359
-42
lines changed

Package.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.7.1
1+
// swift-tools-version: 5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -17,6 +17,10 @@ let package = Package(
1717
.library(
1818
name: "ShopifyAcceleratedCheckouts",
1919
targets: ["ShopifyAcceleratedCheckouts"]
20+
),
21+
.library(
22+
name: "Common",
23+
targets: ["Common"]
2024
)
2125
],
2226
dependencies: [
@@ -28,14 +32,25 @@ let package = Package(
2832
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2933
// Targets can depend on other targets in this package, and on products in packages this package depends on.
3034
.target(
31-
name: "ShopifyCheckoutSheetKit",
35+
name: "Common",
3236
dependencies: []
3337
),
38+
.target(
39+
name: "ShopifyCheckoutSheetKit",
40+
dependencies: ["Common"]
41+
),
3442
.target(
3543
name: "ShopifyAcceleratedCheckouts",
36-
dependencies: ["ShopifyCheckoutSheetKit"],
44+
dependencies: ["ShopifyCheckoutSheetKit", "Common"],
3745
resources: [.process("Localizable.xcstrings")]
3846
),
47+
.testTarget(
48+
name: "CommonTests",
49+
dependencies: ["Common"],
50+
plugins: [
51+
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
52+
]
53+
),
3954
.testTarget(
4055
name: "ShopifyCheckoutSheetKitTests",
4156
dependencies: ["ShopifyCheckoutSheetKit"],

ShopifyCheckoutSheetKit.podspec

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ Pod::Spec.new do |s|
1717
s.swift_version = "5.0"
1818

1919
s.ios.deployment_target = "13.0"
20+
21+
s.pod_target_xcconfig = {
22+
'OTHER_SWIFT_FLAGS' => '-package-name ShopifyCheckoutSheetKit -module-alias Common=ShopifyCheckoutSheetKit'
23+
}
2024

21-
s.source_files = "Sources/ShopifyCheckoutSheetKit/**/*.swift"
25+
s.source_files = "Sources/Common/**/*.swift", "Sources/ShopifyCheckoutSheetKit/**/*.swift"
2226

2327
s.resource_bundles = {
2428
"ShopifyCheckoutSheetKit" => ["Sources/ShopifyCheckoutSheetKit/Assets.xcassets"]

Sources/Common/MetaData.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
package enum MetaData {
27+
/// The version of the `ShopifyCheckoutSheetKit` library.
28+
package static let version = "3.2.0"
29+
/// The schema version of the CheckoutSheetProtocol.
30+
package static let schemaVersion = "8.1"
31+
32+
/// In time this will be used to track the top level package that is
33+
/// making API calls or is the initiator of CSK.
34+
/// For now this is exclusive to AcceleratedCheckouts to ensure backwards
35+
/// compatibility.
36+
package enum EntryPoint: String {
37+
case acceleratedCheckouts = "AcceleratedCheckouts"
38+
}
39+
40+
package enum Platform: String {
41+
case iOS
42+
case reactNative = "ReactNative"
43+
}
44+
}

Sources/Common/UserAgent.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
import UIKit
26+
27+
public enum UserAgent {
28+
public enum ColorScheme: String, CaseIterable {
29+
/// Uses a light, idiomatic color scheme.
30+
case light
31+
/// Uses a dark, idiomatic color scheme.
32+
case dark
33+
/// Infers either `.light` or `.dark` based on the current `UIUserInterfaceStyle`.
34+
case automatic
35+
/// The color scheme presented to buyers using a desktop or mobile browser.
36+
case web = "web_default"
37+
}
38+
39+
package enum CheckoutType {
40+
case standard
41+
case recovery
42+
}
43+
44+
private static let baseUserAgent = "ShopifyCheckoutSDK/\(MetaData.version)"
45+
46+
// Shared format for CheckoutSheetKit and AcceleratedCheckouts
47+
package static func string(
48+
type: CheckoutType,
49+
colorScheme: ColorScheme,
50+
platform: MetaData.Platform? = nil,
51+
entryPoint: MetaData.EntryPoint? = nil
52+
) -> String {
53+
var parameters: String
54+
switch type {
55+
case .standard:
56+
parameters = "\(MetaData.schemaVersion);\(colorScheme.rawValue);standard"
57+
case .recovery:
58+
parameters = "noconnect;\(colorScheme.rawValue);standard_recovery"
59+
}
60+
61+
var userAgentString = "\(baseUserAgent) (\(parameters))"
62+
63+
if let platform {
64+
userAgentString.append(" \(platform.rawValue)")
65+
}
66+
67+
if let entryPoint {
68+
userAgentString.append(" \(entryPoint.rawValue)")
69+
}
70+
71+
return userAgentString
72+
}
73+
}

Sources/ShopifyAcceleratedCheckouts/Internal/GraphQLClient/GraphQLClient.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2222
*/
2323

24+
import Common
2425
import Foundation
2526

2627
/// A lightweight GraphQL client for the Storefront API without external dependencies
@@ -101,6 +102,14 @@ class GraphQLClient {
101102
urlRequest.httpMethod = "POST"
102103
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
103104

105+
// Set User-Agent header
106+
let userAgent = UserAgent.string(
107+
type: .standard,
108+
colorScheme: .automatic,
109+
entryPoint: .acceleratedCheckouts
110+
)
111+
urlRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent")
112+
104113
// Add all provided headers
105114
for (key, value) in headers {
106115
let value = value as String

Sources/ShopifyAcceleratedCheckouts/Wallets/ApplePay/ApplePayViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ protocol PayController: AnyObject {
235235
self.checkoutViewController = ShopifyCheckoutSheetKit.present(
236236
checkout: url,
237237
from: topViewController,
238+
entryPoint: .acceleratedCheckouts,
238239
delegate: self
239240
)
240241
}

Sources/ShopifyAcceleratedCheckouts/Wallets/ShopPay/ShopPayViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import SwiftUI
6262
self.checkoutViewController = ShopifyCheckoutSheetKit.present(
6363
checkout: redirectUrl,
6464
from: topViewController,
65+
entryPoint: .acceleratedCheckouts,
6566
delegate: self
6667
)
6768
}

Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2222
*/
2323

24+
import Common
2425
import WebKit
2526

2627
enum BridgeError: Swift.Error {
@@ -34,29 +35,45 @@ protocol CheckoutBridgeProtocol {
3435
}
3536

3637
enum CheckoutBridge: CheckoutBridgeProtocol {
37-
static let schemaVersion = "8.1"
3838
static let messageHandler = "mobileCheckoutSdk"
39-
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)"
41+
return applicationName(entryPoint: nil)
42+
}
43+
44+
static func applicationName(entryPoint: MetaData.EntryPoint?) -> String {
45+
let colorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
46+
let platform = mapPlatform(ShopifyCheckoutSheetKit.configuration.platform)
4447

45-
return userAgentWithOptionalSuffix(userAgentString)
48+
return UserAgent.string(
49+
type: .standard,
50+
colorScheme: colorScheme,
51+
platform: platform,
52+
entryPoint: entryPoint
53+
)
4654
}
4755

4856
static var recoveryAgent: String {
49-
let theme = ShopifyCheckoutSheetKit.configuration.colorScheme.rawValue
50-
let userAgentString = "\(userAgent) (noconnect;\(theme);standard_recovery)"
57+
return recoveryAgent(entryPoint: nil)
58+
}
5159

52-
return userAgentWithOptionalSuffix(userAgentString)
60+
static func recoveryAgent(entryPoint: MetaData.EntryPoint?) -> String {
61+
let colorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
62+
let platform = mapPlatform(ShopifyCheckoutSheetKit.configuration.platform)
63+
64+
return UserAgent.string(
65+
type: .recovery,
66+
colorScheme: colorScheme,
67+
platform: platform,
68+
entryPoint: entryPoint
69+
)
5370
}
5471

55-
static func userAgentWithOptionalSuffix(_ userAgentString: String) -> String {
56-
if let platform = ShopifyCheckoutSheetKit.configuration.platform?.rawValue {
57-
return "\(userAgentString) \(platform)"
58-
} else {
59-
return userAgentString
72+
private static func mapPlatform(_ platform: Platform?) -> MetaData.Platform? {
73+
guard let platform else { return nil }
74+
switch platform {
75+
case .reactNative:
76+
return .reactNative
6077
}
6178
}
6279

Sources/ShopifyCheckoutSheetKit/CheckoutViewController.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2222
*/
2323

24+
import Common
2425
import SwiftUI
2526
import UIKit
2627

2728
public class CheckoutViewController: UINavigationController {
2829
public init(checkout url: URL, delegate: CheckoutDelegate? = nil) {
29-
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate)
30+
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate, entryPoint: nil)
31+
rootViewController.notifyPresented()
32+
super.init(rootViewController: rootViewController)
33+
presentationController?.delegate = rootViewController
34+
}
35+
36+
package init(checkout url: URL, delegate: CheckoutDelegate? = nil, entryPoint: MetaData.EntryPoint? = nil) {
37+
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate, entryPoint: entryPoint)
3038
rootViewController.notifyPresented()
3139
super.init(rootViewController: rootViewController)
3240
presentationController?.delegate = rootViewController

Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2222
*/
2323

24+
import Common
2425
import UIKit
2526
import WebKit
2627

@@ -62,23 +63,23 @@ class CheckoutWebView: WKWebView {
6263
return !isRecovery && ShopifyCheckoutSheetKit.configuration.preloading.enabled
6364
}
6465

65-
static func `for`(checkout url: URL, recovery: Bool = false) -> CheckoutWebView {
66+
static func `for`(checkout url: URL, recovery: Bool = false, entryPoint: MetaData.EntryPoint? = nil) -> CheckoutWebView {
6667
OSLogger.shared.debug("Creating webview for URL: \(url.absoluteString), recovery: \(recovery)")
6768

6869
if recovery {
6970
CheckoutWebView.invalidate()
70-
return CheckoutWebView(recovery: true)
71+
return CheckoutWebView(recovery: true, entryPoint: entryPoint)
7172
}
7273

73-
let cacheKey = url.absoluteString
74+
let cacheKey = "\(url.absoluteString)_\(entryPoint?.rawValue ?? "nil")"
7475

7576
guard ShopifyCheckoutSheetKit.configuration.preloading.enabled else {
7677
OSLogger.shared.debug("Preloading not enabled")
77-
return uncacheableView()
78+
return uncacheableView(entryPoint: entryPoint)
7879
}
7980

8081
guard let cache, cacheKey == cache.key, !cache.isStale else {
81-
let view = CheckoutWebView()
82+
let view = CheckoutWebView(entryPoint: entryPoint)
8283
CheckoutWebView.cache = CacheEntry(key: cacheKey, view: view)
8384
return view
8485
}
@@ -87,9 +88,9 @@ class CheckoutWebView: WKWebView {
8788
return cache.view
8889
}
8990

90-
static func uncacheableView() -> CheckoutWebView {
91+
static func uncacheableView(entryPoint: MetaData.EntryPoint? = nil) -> CheckoutWebView {
9192
uncacheableViewRef?.detachBridge()
92-
let view = CheckoutWebView()
93+
let view = CheckoutWebView(entryPoint: entryPoint)
9394
uncacheableViewRef = view
9495
return view
9596
}
@@ -128,21 +129,24 @@ class CheckoutWebView: WKWebView {
128129

129130
var isPreloadRequest: Bool = false
130131

132+
private var entryPoint: MetaData.EntryPoint?
133+
131134
// MARK: Initializers
132135

133-
init(frame: CGRect = .zero, configuration: WKWebViewConfiguration = WKWebViewConfiguration(), recovery: Bool = false) {
136+
init(frame: CGRect = .zero, configuration: WKWebViewConfiguration = WKWebViewConfiguration(), recovery: Bool = false, entryPoint: MetaData.EntryPoint? = nil) {
134137
OSLogger.shared.debug("Initializing webview, recovery: \(recovery)")
135138
/// Some external payment providers require ID verification which trigger the camera
136139
/// This configuration option prevents the camera from opening as a "Live Broadcast".
137140
configuration.allowsInlineMediaPlayback = true
141+
self.entryPoint = entryPoint
138142

139143
if recovery {
140144
/// Uses a non-persistent, private cookie store to avoid cross-instance pollution
141145
configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent()
142-
configuration.applicationNameForUserAgent = CheckoutBridge.recoveryAgent
146+
configuration.applicationNameForUserAgent = CheckoutBridge.recoveryAgent(entryPoint: entryPoint)
143147
} else {
144148
/// Set the User-Agent in non-recovery view
145-
configuration.applicationNameForUserAgent = CheckoutBridge.applicationName
149+
configuration.applicationNameForUserAgent = CheckoutBridge.applicationName(entryPoint: entryPoint)
146150
}
147151

148152
isRecovery = recovery

0 commit comments

Comments
 (0)