Skip to content

Commit d7d6aab

Browse files
committed
Add Swift checkout branding configuration
Assisted-By: devx/72c25b65-39e2-427e-9c37-59e2ee7706ec
1 parent f235e96 commit d7d6aab

7 files changed

Lines changed: 112 additions & 12 deletions

File tree

platforms/swift/Sources/ShopifyCheckoutKit/CheckoutProtocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public enum CheckoutProtocol {
77
public typealias Client = EmbeddedCheckoutProtocol.Client
88

99
public static func url(for url: URL) -> URL {
10-
EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: defaultDelegations))
10+
CheckoutURLDecorator.decorate(url)
1111
}
1212

1313
static let defaultDelegations: [EmbeddedCheckoutProtocol.Delegation] = [.windowOpen]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#if !COCOAPODS
2+
import EmbeddedCheckoutProtocol
3+
#endif
4+
import Foundation
5+
6+
enum CheckoutURLDecorator {
7+
static func decorate(
8+
_ url: URL,
9+
configuration: Configuration = ShopifyCheckoutKit.configuration
10+
) -> URL {
11+
let decorated = EmbeddedCheckoutProtocol.url(
12+
for: url,
13+
options: .init(
14+
delegations: CheckoutProtocol.defaultDelegations,
15+
colorScheme: configuration.colorScheme.rawValue
16+
)
17+
)
18+
19+
guard var components = URLComponents(url: decorated, resolvingAgainstBaseURL: false) else {
20+
return decorated
21+
}
22+
23+
var queryItems = components.queryItems ?? []
24+
queryItems.removeAll { $0.name == Self.brandingQueryItemName }
25+
queryItems.append(URLQueryItem(name: Self.brandingQueryItemName, value: configuration.colorScheme.brandingValue))
26+
components.queryItems = queryItems
27+
28+
return components.url ?? decorated
29+
}
30+
31+
private static let brandingQueryItemName = "ck_branding"
32+
}
33+
34+
private extension Configuration.ColorScheme {
35+
var brandingValue: String {
36+
switch self {
37+
case .web:
38+
return "shop"
39+
case .automatic, .dark, .light:
40+
return "app"
41+
}
42+
}
43+
}

platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
3333
var onFailAction: ((CheckoutError) -> Void)?
3434

3535
public init(checkout url: URL) {
36-
checkoutURL = CheckoutProtocol.url(for: url)
36+
checkoutURL = CheckoutURLDecorator.decorate(url)
3737
}
3838

3939
public func makeUIViewController(context _: Self.Context) -> CheckoutViewController {

platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public func preload(checkout url: URL) {
4848
return
4949
}
5050

51-
let decorated = CheckoutProtocol.url(for: url)
51+
let decorated = CheckoutURLDecorator.decorate(url)
5252
CheckoutWebView.preload(checkout: decorated)
5353
}
5454

@@ -61,7 +61,7 @@ public func invalidate() {
6161
@MainActor
6262
@discardableResult
6363
public func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
64-
let decorated = CheckoutProtocol.url(for: url)
64+
let decorated = CheckoutURLDecorator.decorate(url)
6565
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client)
6666
from.present(viewController, animated: true)
6767
return viewController
@@ -70,7 +70,7 @@ public func present(checkout url: URL, from: UIViewController, delegate: (any Ch
7070
@MainActor
7171
@discardableResult
7272
package func present(checkout url: URL, from: UIViewController, entryPoint: MetaData.EntryPoint, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
73-
let decorated = CheckoutProtocol.url(for: url)
73+
let decorated = CheckoutURLDecorator.decorate(url)
7474
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client, entryPoint: entryPoint)
7575
from.present(viewController, animated: true)
7676
return viewController
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Foundation
2+
@testable import ShopifyCheckoutKit
3+
import Testing
4+
5+
@Suite("Checkout URL Decoration")
6+
struct CheckoutURLDecoratorTests {
7+
@Test func appendsColorSchemeAndDerivedAppBranding() throws {
8+
var configuration = Configuration()
9+
configuration.colorScheme = .dark
10+
11+
let url = try #require(URL(string: "https://shop.com/cart/c/abc?key=cart_token"))
12+
let items = queryItems(CheckoutURLDecorator.decorate(url, configuration: configuration))
13+
14+
#expect(items.first(where: { $0.name == "key" })?.value == "cart_token")
15+
#expect(items.first(where: { $0.name == "ec_color_scheme" })?.value == "dark")
16+
#expect(items.first(where: { $0.name == "ck_branding" })?.value == "app")
17+
}
18+
19+
@Test func replacesCallerSuppliedBrandingAndIsIdempotent() throws {
20+
var configuration = Configuration()
21+
configuration.colorScheme = .light
22+
23+
let url = try #require(URL(string: "https://shop.com/cart/c/abc?ck_branding=app&ec_color_scheme=dark"))
24+
let once = CheckoutURLDecorator.decorate(url, configuration: configuration)
25+
let twice = CheckoutURLDecorator.decorate(once, configuration: configuration)
26+
let items = queryItems(twice)
27+
28+
#expect(items.filter { $0.name == "ck_branding" }.map(\.value) == ["app"])
29+
#expect(items.filter { $0.name == "ec_color_scheme" }.map(\.value) == ["light"])
30+
}
31+
32+
@Test func derivesBrandingForEachColorScheme() throws {
33+
try assertColorSchemeDecoratesWith(.light, colorScheme: "light", branding: "app")
34+
try assertColorSchemeDecoratesWith(.dark, colorScheme: "dark", branding: "app")
35+
try assertColorSchemeDecoratesWith(.automatic, colorScheme: "automatic", branding: "app")
36+
try assertColorSchemeDecoratesWith(.web, colorScheme: "web_default", branding: "shop")
37+
}
38+
39+
private func assertColorSchemeDecoratesWith(
40+
_ colorScheme: Configuration.ColorScheme,
41+
colorScheme expectedColorScheme: String,
42+
branding expectedBranding: String
43+
) throws {
44+
var configuration = Configuration()
45+
configuration.colorScheme = colorScheme
46+
47+
let url = try #require(URL(string: "https://shop.com/cart/c/abc"))
48+
let items = queryItems(CheckoutURLDecorator.decorate(url, configuration: configuration))
49+
50+
#expect(items.first(where: { $0.name == "ec_color_scheme" })?.value == expectedColorScheme)
51+
#expect(items.first(where: { $0.name == "ck_branding" })?.value == expectedBranding)
52+
}
53+
54+
private func queryItems(_ url: URL) -> [URLQueryItem] {
55+
URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
56+
}
57+
}

platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewControllerTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class CheckoutWebViewControllerTests: XCTestCase {
7575
func test_presentationControllerDidDismiss_doesNotCleanUpBeforeViewDisappears() throws {
7676
ShopifyCheckoutKit.configuration.preloading.enabled = true
7777
ShopifyCheckoutKit.preload(checkout: url)
78-
let viewController = TestableCheckoutWebViewController(checkoutURL: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)), entryPoint: nil)
78+
let viewController = TestableCheckoutWebViewController(checkoutURL: CheckoutURLDecorator.decorate(url), entryPoint: nil)
7979
viewController.loadViewIfNeeded()
8080

8181
let checkoutView = try XCTUnwrap(viewController.checkoutView)
@@ -93,7 +93,7 @@ class CheckoutWebViewControllerTests: XCTestCase {
9393
func test_viewDidDisappear_cleansUpConsumedPreloadedWebViewWhenDismissed() throws {
9494
ShopifyCheckoutKit.configuration.preloading.enabled = true
9595
ShopifyCheckoutKit.preload(checkout: url)
96-
let viewController = TestableCheckoutWebViewController(checkoutURL: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)), entryPoint: nil)
96+
let viewController = TestableCheckoutWebViewController(checkoutURL: CheckoutURLDecorator.decorate(url), entryPoint: nil)
9797
viewController.loadViewIfNeeded()
9898

9999
let checkoutView = try XCTUnwrap(viewController.checkoutView)

platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ class CheckoutWebViewTests: XCTestCase {
284284
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())
285285
XCTAssertTrue(CheckoutWebView.preloadCache.hasActiveKeepAlive())
286286

287-
let cached = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
287+
let cached = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
288288

289289
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())
290290
XCTAssertFalse(CheckoutWebView.preloadCache.hasActiveKeepAlive())
@@ -305,8 +305,8 @@ class CheckoutWebViewTests: XCTestCase {
305305

306306
func testPresentingMatchingCheckoutReusesCachedWebViewWithoutEvictingIt() {
307307
ShopifyCheckoutKit.preload(checkout: url)
308-
let first = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
309-
let second = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
308+
let first = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
309+
let second = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
310310

311311
XCTAssertTrue(first === second)
312312
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())
@@ -411,7 +411,7 @@ class CheckoutWebViewTests: XCTestCase {
411411

412412
func testInvalidateDetachesCachedPreloadedWebView() {
413413
ShopifyCheckoutKit.preload(checkout: url)
414-
let cached = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
414+
let cached = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
415415
XCTAssertTrue(cached.isBridgeAttached)
416416

417417
ShopifyCheckoutKit.invalidate()
@@ -422,7 +422,7 @@ class CheckoutWebViewTests: XCTestCase {
422422

423423
func testHTTPErrorInvalidatesPreloadCache() throws {
424424
ShopifyCheckoutKit.preload(checkout: url)
425-
let cached = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
425+
let cached = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
426426
let link = try XCTUnwrap(cached.url)
427427
let response = try XCTUnwrap(HTTPURLResponse(url: link, statusCode: 403, httpVersion: nil, headerFields: nil))
428428

0 commit comments

Comments
 (0)