Skip to content
Draft
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,14 +7,16 @@ import UIKit
@MainActor
public class CheckoutViewController: UINavigationController {
public init(checkout url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) {
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate, client: client, entryPoint: nil)
let decoratedURL = CheckoutURLDecorator.decorate(url)
let rootViewController = CheckoutWebViewController(checkoutURL: decoratedURL, delegate: delegate, client: client, entryPoint: nil)
super.init(rootViewController: rootViewController)
configureNavigationBar()
presentationController?.delegate = rootViewController
}

package init(checkout url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) {
let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate, client: client, entryPoint: entryPoint)
let decoratedURL = CheckoutURLDecorator.decorate(url)
let rootViewController = CheckoutWebViewController(checkoutURL: decoratedURL, delegate: delegate, client: client, entryPoint: entryPoint)
super.init(rootViewController: rootViewController)
configureNavigationBar()
presentationController?.delegate = rootViewController
Expand Down Expand Up @@ -48,12 +50,8 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
checkoutURL = url
}

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

public func makeUIViewController(context _: Self.Context) -> CheckoutViewController {
let viewController = CheckoutViewController(checkout: decoratedCheckoutURL, client: client)
let viewController = CheckoutViewController(checkout: checkoutURL, client: client)
configureWebViewController(viewController)
return viewController
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,15 @@ public func invalidate() {
@MainActor
@discardableResult
public func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
let decorated = CheckoutURLDecorator.decorate(url)
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client)
let viewController = CheckoutViewController(checkout: url, delegate: delegate, client: client)
from.present(viewController, animated: true)
return viewController
}

@MainActor
@discardableResult
package func present(checkout url: URL, from: UIViewController, entryPoint: MetaData.EntryPoint, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController {
let decorated = CheckoutURLDecorator.decorate(url)
let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client, entryPoint: entryPoint)
let viewController = CheckoutViewController(checkout: url, delegate: delegate, client: client, entryPoint: entryPoint)
from.present(viewController, animated: true)
return viewController
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ struct CheckoutProtocolTests {
#expect(CheckoutProtocol.defaultDelegations == ["window.open"])
}

@Test func urlDecoratesCheckoutForEmbeddedProtocol() throws {
let url = try #require(URL(string: "https://shop.com/cart/c/abc?key=cart_token"))
let decoratedURL = CheckoutProtocol.url(for: url)
let items = try #require(URLComponents(url: decoratedURL, resolvingAgainstBaseURL: false)?.queryItems)

#expect(items.filter { $0.name == "key" }.map(\.value) == ["cart_token"])
#expect(items.filter { $0.name == "ec_version" }.map(\.value) == [EmbeddedCheckoutProtocol.specVersion])
#expect(items.filter { $0.name == "ec_delegate" }.map(\.value) == ["window.open"])
#expect(items.filter { $0.name == "ec_color_scheme" }.count == 1)
#expect(items.filter { $0.name == "ck_branding" }.count == 1)
}

@Test func supportedProtocolMethodsCoverReadyCuratedNotificationsAndWindowOpen() {
#expect(CheckoutProtocol.supportedProtocolMethods == [
"ec.ready",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import EmbeddedCheckoutProtocol
import Foundation
@testable import ShopifyCheckoutKit
import Testing
Expand All @@ -11,22 +12,26 @@ struct CheckoutURLDecoratorTests {
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")
#expect(items.filter { $0.name == "key" }.map(\.value) == ["cart_token"])
#expect(items.filter { $0.name == "ec_version" }.map(\.value) == [EmbeddedCheckoutProtocol.specVersion])
#expect(items.filter { $0.name == "ec_delegate" }.map(\.value) == ["window.open"])
#expect(items.filter { $0.name == "ec_color_scheme" }.map(\.value) == ["dark"])
#expect(items.filter { $0.name == "ck_branding" }.map(\.value) == ["app"])
}

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

let url = try #require(URL(string: "https://shop.com/cart/c/abc?ck_branding=app&ec_color_scheme=dark"))
let url = try #require(URL(string: "https://shop.com/cart/c/abc?ec_version=stale&ec_delegate=custom&ck_branding=shop&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_version" }.map(\.value) == [EmbeddedCheckoutProtocol.specVersion])
#expect(items.filter { $0.name == "ec_delegate" }.map(\.value) == ["window.open"])
#expect(items.filter { $0.name == "ec_color_scheme" }.map(\.value) == ["light"])
#expect(items.filter { $0.name == "ck_branding" }.map(\.value) == ["app"])
}

@Test func derivesCheckoutParamsForEachAppearance() throws {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import EmbeddedCheckoutProtocol
import Foundation
@testable import ShopifyCheckoutKit
import XCTest

func assertDecoratedCheckoutURL(
_ url: URL?,
colorScheme: String = "dark",
branding: String = "app",
file: StaticString = #filePath,
line: UInt = #line
) throws {
let url = try XCTUnwrap(url, file: file, line: line)
let queryItems = try XCTUnwrap(
URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
file: file,
line: line
)

XCTAssertEqual(queryItems.filter { $0.name == "ec_version" }.map(\.value), [EmbeddedCheckoutProtocol.specVersion], file: file, line: line)
XCTAssertEqual(queryItems.filter { $0.name == "ec_delegate" }.map(\.value), ["window.open"], file: file, line: line)
XCTAssertEqual(queryItems.filter { $0.name == "ec_color_scheme" }.map(\.value), [colorScheme], file: file, line: line)
XCTAssertEqual(queryItems.filter { $0.name == "ck_branding" }.map(\.value), [branding], file: file, line: line)
XCTAssertEqual(queryItems.filter { $0.name == "key" }.map(\.value), ["cart_token"], file: file, line: line)
}

@MainActor
func loadedCheckoutURL(from viewController: CheckoutViewController) throws -> URL {
let webViewController = try XCTUnwrap(
viewController.viewControllers.compactMap { $0 as? CheckoutWebViewController }.first
)
webViewController.loadViewIfNeeded()
return try XCTUnwrap(webViewController.checkoutView?.loadedCheckoutURL)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import XCTest

@MainActor
class ShopifyCheckoutKitTests: XCTestCase {
let checkoutURL = URL(string: "https://shop.example/checkouts/cn/123")!
let checkoutURL = URL(string: "https://shop.example/checkouts/cn/123?key=cart_token")!

private var originalConfiguration: Configuration!

override func setUp() async throws {
try await super.setUp()
originalConfiguration = ShopifyCheckoutKit.configuration
ShopifyCheckoutKit.configuration.appearance = .app(.dark)
CheckoutWebView.invalidate()
}

Expand Down Expand Up @@ -63,6 +64,15 @@ class ShopifyCheckoutKitTests: XCTestCase {
)
}

func test_present_decoratesCheckoutURL() throws {
let viewController = ShopifyCheckoutKit.present(
checkout: checkoutURL,
from: UIViewController()
)

try assertDecoratedCheckoutURL(loadedCheckoutURL(from: viewController))
}

func test_present_propagatesDelegateAndClientToWebViewController() throws {
let delegate = MockCheckoutDelegate()
let client = MockBridgeClient()
Expand Down Expand Up @@ -118,6 +128,20 @@ class ShopifyCheckoutKitTests: XCTestCase {
XCTAssertNil(ShopifyCheckoutKit.preload(checkout: checkoutURL))
}

func test_preload_decoratesCheckoutURL() throws {
ShopifyCheckoutKit.configuration.preloading.enabled = true
let preload = ShopifyCheckoutKit.preload(checkout: checkoutURL)
let expectedURL = CheckoutURLDecorator.decorate(checkoutURL)
let checkoutView = try XCTUnwrap(
CheckoutWebView.preloadCache.view(
for: PreloadKey(url: expectedURL, entryPoint: nil)
)
)

try assertDecoratedCheckoutURL(checkoutView.loadedCheckoutURL)
withExtendedLifetime(preload) {}
}

func test_preload_returnsPreloadWhenEnabled() {
ShopifyCheckoutKit.configuration.preloading.enabled = true
let preload = ShopifyCheckoutKit.preload(checkout: checkoutURL)
Expand Down
63 changes: 53 additions & 10 deletions platforms/swift/Tests/ShopifyCheckoutKitTests/SwiftUITests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@testable import ShopifyCheckoutKit
import SwiftUI
import XCTest

@MainActor
Expand All @@ -8,13 +9,34 @@ class CheckoutViewControllerTests: XCTestCase {

override func setUp() async throws {
try await super.setUp()
checkoutURL = URL(string: "https://www.shopify.com")
ShopifyCheckoutKit.configuration = Configuration()
ShopifyCheckoutKit.configuration.appearance = .app(.dark)
ShopifyCheckoutKit.configuration.preloading.enabled = false
checkoutURL = URL(string: "https://www.shopify.com?key=cart_token")
checkoutViewController = CheckoutViewController(checkout: checkoutURL)
}

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

func testInit() {
XCTAssertNotNil(checkoutViewController)
}

func testInitDecoratesCheckoutURL() throws {
try assertDecoratedCheckoutURL(loadedCheckoutURL(from: checkoutViewController))
}

func testEntryPointInitDecoratesCheckoutURL() throws {
let viewController = CheckoutViewController(
checkout: checkoutURL,
entryPoint: .acceleratedCheckouts
)

try assertDecoratedCheckoutURL(loadedCheckoutURL(from: viewController))
}
}

@MainActor
Expand All @@ -25,7 +47,9 @@ class ShopifyCheckoutTests: XCTestCase {
override func setUp() async throws {
try await super.setUp()
ShopifyCheckoutKit.configuration = Configuration()
checkoutURL = URL(string: "https://www.shopify.com")
ShopifyCheckoutKit.configuration.appearance = .app(.dark)
ShopifyCheckoutKit.configuration.preloading.enabled = false
checkoutURL = URL(string: "https://www.shopify.com?key=cart_token")
shopifyCheckout = ShopifyCheckout(checkout: checkoutURL)
}

Expand Down Expand Up @@ -64,6 +88,33 @@ class ShopifyCheckoutTests: XCTestCase {
let sheet = shopifyCheckout.connect(client)
XCTAssertNotNil(sheet.client)
}

func testCheckoutViewControllerDecoratesCheckoutURL() async throws {
let hostingController = UIHostingController(rootView: shopifyCheckout)
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = hostingController
window.makeKeyAndVisible()
hostingController.loadViewIfNeeded()

var descendant: CheckoutViewController?
for _ in 0 ..< 10 where descendant == nil {
hostingController.view.layoutIfNeeded()
descendant = descendantCheckoutViewController(from: hostingController)
await Task.yield()
}

let checkoutViewController = try XCTUnwrap(descendant)
try assertDecoratedCheckoutURL(loadedCheckoutURL(from: checkoutViewController))
withExtendedLifetime(window) {}
}

private func descendantCheckoutViewController(from viewController: UIViewController) -> CheckoutViewController? {
if let checkoutViewController = viewController as? CheckoutViewController {
return checkoutViewController
}

return viewController.children.lazy.compactMap(descendantCheckoutViewController).first
}
}

@MainActor
Expand Down Expand Up @@ -95,14 +146,6 @@ class CheckoutConfigurableTests: XCTestCase {
XCTAssertEqual(ShopifyCheckoutKit.configuration.appearance, appearance)
}

func testAppearanceDecoratesCheckoutURLAfterModifierRuns() throws {
let sheet = shopifyCheckout.appearance(.storefront)
let items = try XCTUnwrap(URLComponents(url: sheet.decoratedCheckoutURL, resolvingAgainstBaseURL: false)?.queryItems)

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

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