Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public enum CheckoutProtocol {
public typealias Client = EmbeddedCheckoutProtocol.Client

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

static let defaultDelegations: [EmbeddedCheckoutProtocol.Delegation] = [.windowOpen]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#if !COCOAPODS
import EmbeddedCheckoutProtocol
#endif
import Foundation

enum CheckoutURLDecorator {
static func decorate(
_ url: URL,
configuration: Configuration = ShopifyCheckoutKit.configuration
) -> URL {
let decorated = EmbeddedCheckoutProtocol.url(
for: url,
options: .init(
delegations: CheckoutProtocol.defaultDelegations,
colorScheme: configuration.colorScheme.rawValue
)
)

guard var components = URLComponents(url: decorated, resolvingAgainstBaseURL: false) else {
return decorated
}

var queryItems = components.queryItems ?? []
queryItems.removeAll { $0.name == Self.brandingQueryItemName }
queryItems.append(URLQueryItem(name: Self.brandingQueryItemName, value: configuration.colorScheme.brandingValue))
components.queryItems = queryItems

return components.url ?? decorated
}

private static let brandingQueryItemName = "ck_branding"
}

extension Configuration.ColorScheme {
fileprivate var brandingValue: String {
switch self {
case .web:
return "shop"
case .automatic, .dark, .light:
return "app"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
var onFailAction: ((CheckoutError) -> Void)?

public init(checkout url: URL) {
checkoutURL = CheckoutProtocol.url(for: url)
checkoutURL = url
}

var decoratedCheckoutURL: URL {
CheckoutURLDecorator.decorate(checkoutURL)
}

public func makeUIViewController(context _: Self.Context) -> CheckoutViewController {
let viewController = CheckoutViewController(checkout: checkoutURL, client: client)
let viewController = CheckoutViewController(checkout: decoratedCheckoutURL, client: client)
configureWebViewController(viewController)
return viewController
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public func preload(checkout url: URL) {
return
}

let decorated = CheckoutProtocol.url(for: url)
let decorated = CheckoutURLDecorator.decorate(url)
CheckoutWebView.preload(checkout: decorated)
}

Expand All @@ -61,7 +61,7 @@ public func invalidate() {
@MainActor
@discardableResult
public func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
let decorated = CheckoutProtocol.url(for: url)
let decorated = CheckoutURLDecorator.decorate(url)
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client)
from.present(viewController, animated: true)
return viewController
Expand All @@ -70,7 +70,7 @@ public func present(checkout url: URL, from: UIViewController, delegate: (any Ch
@MainActor
@discardableResult
package func present(checkout url: URL, from: UIViewController, entryPoint: MetaData.EntryPoint, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
let decorated = CheckoutProtocol.url(for: url)
let decorated = CheckoutURLDecorator.decorate(url)
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client, entryPoint: entryPoint)
from.present(viewController, animated: true)
return viewController
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation
@testable import ShopifyCheckoutKit
import Testing

@Suite("Checkout URL Decoration")
struct CheckoutURLDecoratorTests {
@Test func appendsColorSchemeAndDerivedAppBranding() throws {
var configuration = Configuration()
configuration.colorScheme = .dark

let url = try #require(URL(string: "https://shop.com/cart/c/abc?key=cart_token"))
let items = queryItems(CheckoutURLDecorator.decorate(url, configuration: configuration))

#expect(items.first(where: { $0.name == "key" })?.value == "cart_token")
#expect(items.first(where: { $0.name == "ec_color_scheme" })?.value == "dark")
#expect(items.first(where: { $0.name == "ck_branding" })?.value == "app")
}

@Test func replacesCallerSuppliedBrandingAndIsIdempotent() throws {
var configuration = Configuration()
configuration.colorScheme = .light

let url = try #require(URL(string: "https://shop.com/cart/c/abc?ck_branding=app&ec_color_scheme=dark"))
let once = CheckoutURLDecorator.decorate(url, configuration: configuration)
let twice = CheckoutURLDecorator.decorate(once, configuration: configuration)
let items = queryItems(twice)

#expect(items.filter { $0.name == "ck_branding" }.map(\.value) == ["app"])
#expect(items.filter { $0.name == "ec_color_scheme" }.map(\.value) == ["light"])
}

@Test func derivesBrandingForEachColorScheme() throws {
try assertColorSchemeDecoratesWith(.light, colorScheme: "light", branding: "app")
try assertColorSchemeDecoratesWith(.dark, colorScheme: "dark", branding: "app")
try assertColorSchemeDecoratesWith(.automatic, colorScheme: "automatic", branding: "app")
try assertColorSchemeDecoratesWith(.web, colorScheme: "web_default", branding: "shop")
}

private func assertColorSchemeDecoratesWith(
_ colorScheme: Configuration.ColorScheme,
colorScheme expectedColorScheme: String,
branding expectedBranding: String
) throws {
var configuration = Configuration()
configuration.colorScheme = colorScheme

let url = try #require(URL(string: "https://shop.com/cart/c/abc"))
let items = queryItems(CheckoutURLDecorator.decorate(url, configuration: configuration))

#expect(items.first(where: { $0.name == "ec_color_scheme" })?.value == expectedColorScheme)
#expect(items.first(where: { $0.name == "ck_branding" })?.value == expectedBranding)
}

private func queryItems(_ url: URL) -> [URLQueryItem] {
URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class CheckoutWebViewControllerTests: XCTestCase {
func test_presentationControllerDidDismiss_doesNotCleanUpBeforeViewDisappears() throws {
ShopifyCheckoutKit.configuration.preloading.enabled = true
ShopifyCheckoutKit.preload(checkout: url)
let viewController = TestableCheckoutWebViewController(checkoutURL: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)), entryPoint: nil)
let viewController = TestableCheckoutWebViewController(checkoutURL: CheckoutURLDecorator.decorate(url), entryPoint: nil)
viewController.loadViewIfNeeded()

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

let checkoutView = try XCTUnwrap(viewController.checkoutView)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ class CheckoutWebViewTests: XCTestCase {
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())
XCTAssertTrue(CheckoutWebView.preloadCache.hasActiveKeepAlive())

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

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

func testPresentingMatchingCheckoutReusesCachedWebViewWithoutEvictingIt() {
ShopifyCheckoutKit.preload(checkout: url)
let first = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
let second = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
let first = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
let second = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))

XCTAssertTrue(first === second)
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())
Expand Down Expand Up @@ -411,7 +411,7 @@ class CheckoutWebViewTests: XCTestCase {

func testInvalidateDetachesCachedPreloadedWebView() {
ShopifyCheckoutKit.preload(checkout: url)
let cached = CheckoutWebView.for(checkout: EmbeddedCheckoutProtocol.url(for: url, options: .init(delegations: CheckoutProtocol.defaultDelegations)))
let cached = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(url))
XCTAssertTrue(cached.isBridgeAttached)

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ class ShopifyCheckoutTests: XCTestCase {

override func setUp() async throws {
try await super.setUp()
ShopifyCheckoutKit.configuration = Configuration()
checkoutURL = URL(string: "https://www.shopify.com")
shopifyCheckout = ShopifyCheckout(checkout: checkoutURL)
}

override func tearDown() async throws {
ShopifyCheckoutKit.configuration = Configuration()
try await super.tearDown()
}

func testOnCancel() {
var cancelActionCalled = false

Expand Down Expand Up @@ -67,10 +73,16 @@ class CheckoutConfigurableTests: XCTestCase {

override func setUp() async throws {
try await super.setUp()
ShopifyCheckoutKit.configuration = Configuration()
checkoutURL = URL(string: "https://www.shopify.com")
shopifyCheckout = ShopifyCheckout(checkout: checkoutURL)
}

override func tearDown() async throws {
ShopifyCheckoutKit.configuration = Configuration()
try await super.tearDown()
}

func testBackgroundColor() {
let color = UIColor.red
shopifyCheckout.backgroundColor(color)
Expand All @@ -83,6 +95,14 @@ class CheckoutConfigurableTests: XCTestCase {
XCTAssertEqual(ShopifyCheckoutKit.configuration.colorScheme, colorScheme)
}

func testColorSchemeDecoratesCheckoutURLAfterModifierRuns() throws {
let sheet = shopifyCheckout.colorScheme(.web)
let items = try XCTUnwrap(URLComponents(url: sheet.decoratedCheckoutURL, resolvingAgainstBaseURL: false)?.queryItems)

XCTAssertEqual(items.first(where: { $0.name == "ec_color_scheme" })?.value, "web_default")
XCTAssertEqual(items.first(where: { $0.name == "ck_branding" })?.value, "shop")
}

func testTintColor() {
let color = UIColor.blue
shopifyCheckout.tintColor(color)
Expand Down
Loading