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
2 changes: 2 additions & 0 deletions platforms/swift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ ShopifyCheckoutKit.configure {
}
```

SwiftUI modifiers such as `.appearance(...)`, `.tintColor(...)`, and `.title(...)` override these defaults only for that `ShopifyCheckout` value. They do not mutate `ShopifyCheckoutKit.configuration` or invalidate a cached preload.

| Option | Default | Purpose |
| --- | --- | --- |
| `appearance` | `.storefront` | Match the storefront's web checkout branding with a light color scheme, or use the Checkout Kit style with `.app(.automatic)`, `.app(.light)`, or `.app(.dark)`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,39 @@ 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 rootViewController = CheckoutWebViewController(
checkoutURL: url,
configuration: ShopifyCheckoutKit.configuration,
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 rootViewController = CheckoutWebViewController(
checkoutURL: url,
configuration: ShopifyCheckoutKit.configuration,
delegate: delegate,
client: client,
entryPoint: entryPoint
)
super.init(rootViewController: rootViewController)
configureNavigationBar()
presentationController?.delegate = rootViewController
}

init(checkout url: URL, configuration: Configuration, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) {
let rootViewController = CheckoutWebViewController(
checkoutURL: url,
configuration: configuration,
delegate: delegate,
client: client,
entryPoint: entryPoint
)
super.init(rootViewController: rootViewController)
configureNavigationBar()
presentationController?.delegate = rootViewController
Expand All @@ -40,20 +65,26 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
public typealias UIViewControllerType = CheckoutViewController

var checkoutURL: URL
var configuration: Configuration
var client: (any CheckoutCommunicationProtocol)?
var onDismissAction: (() -> Void)?
var onFailAction: ((CheckoutError) -> Void)?

public init(checkout url: URL) {
checkoutURL = url
configuration = ShopifyCheckoutKit.configuration
}

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

public func makeUIViewController(context _: Self.Context) -> CheckoutViewController {
let viewController = CheckoutViewController(checkout: decoratedCheckoutURL, client: client)
let viewController = CheckoutViewController(
checkout: decoratedCheckoutURL,
configuration: configuration,
client: client
)
configureWebViewController(viewController)
return viewController
}
Expand Down Expand Up @@ -111,27 +142,31 @@ public protocol CheckoutConfigurable {

extension CheckoutConfigurable {
@discardableResult public func backgroundColor(_ color: UIColor) -> Self {
ShopifyCheckoutKit.configuration.backgroundColor = color
return self
modifyingConfiguration { $0.backgroundColor = color }
}

@discardableResult public func appearance(_ appearance: ShopifyCheckoutKit.Configuration.Appearance) -> Self {
ShopifyCheckoutKit.configuration.appearance = appearance
return self
modifyingConfiguration { $0.appearance = appearance }
}

@discardableResult public func tintColor(_ color: UIColor) -> Self {
ShopifyCheckoutKit.configuration.tintColor = color
return self
modifyingConfiguration { $0.tintColor = color }
}

@discardableResult public func title(_ title: String) -> Self {
ShopifyCheckoutKit.configuration.title = title
return self
modifyingConfiguration { $0.title = title }
}

@discardableResult public func closeButtonTintColor(_ color: UIColor?) -> Self {
ShopifyCheckoutKit.configuration.closeButtonTintColor = color
return self
modifyingConfiguration { $0.closeButtonTintColor = color }
}

private func modifyingConfiguration(_ update: (inout Configuration) -> Void) -> Self {
guard var copy = self as? ShopifyCheckout else {
return self
}

update(&copy.configuration)
return copy as? Self ?? self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl
var checkoutView: CheckoutWebView?

lazy var progressBar: ProgressBarView = {
let progressBar = ProgressBarView(frame: .zero)
let progressBar = ProgressBarView(frame: .zero, tintColor: configuration.tintColor)
progressBar.translatesAutoresizingMaskIntoConstraints = false
return progressBar
}()

var initialNavigation: Bool = true

private let checkoutURL: URL
private let configuration: Configuration

private lazy var closeBarButtonItem: UIBarButtonItem = {
if let closeButtonTintColor = ShopifyCheckoutKit.configuration.closeButtonTintColor {
if let closeButtonTintColor = configuration.closeButtonTintColor {
var item: UIBarButtonItem

if #available(iOS 26.0, *) {
Expand Down Expand Up @@ -62,26 +63,29 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl

// MARK: Initializers

public init(checkoutURL url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) {
public init(checkoutURL url: URL, configuration: Configuration = ShopifyCheckoutKit.configuration, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) {
checkoutURL = url
self.configuration = configuration
self.delegate = delegate
self.client = client

let checkoutView = CheckoutWebView.for(checkout: url, entryPoint: entryPoint)
checkoutView.backgroundColor = configuration.backgroundColor
checkoutView.underPageBackgroundColor = configuration.backgroundColor
checkoutView.translatesAutoresizingMaskIntoConstraints = false
checkoutView.scrollView.contentInsetAdjustmentBehavior = .automatic
checkoutView.client = client
self.checkoutView = checkoutView

super.init(nibName: nil, bundle: nil)

title = ShopifyCheckoutKit.configuration.title
title = configuration.title

navigationItem.rightBarButtonItem = closeBarButtonItem

checkoutView.viewDelegate = self

view.backgroundColor = ShopifyCheckoutKit.configuration.backgroundColor
view.backgroundColor = configuration.backgroundColor
}

@available(*, unavailable)
Expand All @@ -94,7 +98,7 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

view.backgroundColor = ShopifyCheckoutKit.configuration.backgroundColor
view.backgroundColor = configuration.backgroundColor
}

override public func viewDidLoad() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ class ProgressBarView: UIView {
}()

private var progressAnimation: UIViewPropertyAnimator?
private let configuredTintColor: UIColor

override init(frame: CGRect) {
init(frame: CGRect, tintColor: UIColor) {
configuredTintColor = tintColor
super.init(frame: frame)

addSubview(progressBar)
Expand All @@ -21,7 +23,7 @@ class ProgressBarView: UIView {
progressBar.heightAnchor.constraint(equalToConstant: 1)
])

progressBar.tintColor = ShopifyCheckoutKit.configuration.tintColor
progressBar.tintColor = configuredTintColor
}

override func didMoveToSuperview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ class CheckoutViewDelegateTests: XCTestCase {
XCTAssertEqual(viewController.title, "Custom title")
}

func testInstanceConfigurationIsAppliedToCheckoutChrome() throws {
var configuration = Configuration()
configuration.backgroundColor = .red
configuration.tintColor = .blue
configuration.title = "Instance checkout"
configuration.closeButtonTintColor = .green

let controller = MockCheckoutWebViewController(
checkoutURL: checkoutURL,
configuration: configuration
)
controller.loadViewIfNeeded()

XCTAssertEqual(controller.title, "Instance checkout")
assertColor(controller.view.backgroundColor, equals: .red)
assertColor(controller.checkoutView?.backgroundColor, equals: .red)
assertColor(controller.checkoutView?.underPageBackgroundColor, equals: .red)
assertColor(controller.progressBar.progressBar.tintColor, equals: .blue)
let closeButton = try XCTUnwrap(controller.navigationItem.rightBarButtonItem)
assertColor(closeButton.tintColor, equals: .green)
XCTAssertNotNil(closeButton.image)
}

func testCheckoutViewDidFailWithErrorDismissesViewController() {
viewController.checkoutViewDidFailWithError(error: CheckoutError(code: .httpError, message: "error", httpStatusCode: 500))

Expand Down Expand Up @@ -125,6 +148,13 @@ class CheckoutViewDelegateTests: XCTestCase {
let closeButton = controller.navigationItem.rightBarButtonItem
XCTAssertNotNil(closeButton?.image)
}

private func assertColor(_ actual: UIColor?, equals expected: UIColor, file: StaticString = #filePath, line: UInt = #line) {
let actualComponents = actual?.cgColor.components
let expectedComponents = expected.cgColor.components

XCTAssertEqual(actualComponents, expectedComponents, file: file, line: line)
}
}

@MainActor
Expand Down
87 changes: 66 additions & 21 deletions platforms/swift/Tests/ShopifyCheckoutKitTests/SwiftUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,46 +83,91 @@ class CheckoutConfigurableTests: XCTestCase {
try await super.tearDown()
}

func testBackgroundColor() {
func testBackgroundColorIsCapturedWithoutChangingGlobalConfiguration() {
let globalColor = ShopifyCheckoutKit.configuration.backgroundColor
let color = UIColor.red
shopifyCheckout.backgroundColor(color)
XCTAssertEqual(ShopifyCheckoutKit.configuration.backgroundColor, color)

let sheet = shopifyCheckout.backgroundColor(color)

XCTAssertEqual(sheet.configuration.backgroundColor, color)
XCTAssertEqual(shopifyCheckout.configuration.backgroundColor, globalColor)
XCTAssertEqual(ShopifyCheckoutKit.configuration.backgroundColor, globalColor)
}

func testAppearance() {
func testAppearanceIsCapturedWithoutChangingGlobalConfiguration() {
let globalAppearance = ShopifyCheckoutKit.configuration.appearance
let appearance = ShopifyCheckoutKit.Configuration.Appearance.app(.light)
shopifyCheckout.appearance(appearance)
XCTAssertEqual(ShopifyCheckoutKit.configuration.appearance, appearance)

let sheet = shopifyCheckout.appearance(appearance)

XCTAssertEqual(sheet.configuration.appearance, appearance)
XCTAssertEqual(shopifyCheckout.configuration.appearance, globalAppearance)
XCTAssertEqual(ShopifyCheckoutKit.configuration.appearance, globalAppearance)
}

func testAppearanceDecoratesCheckoutURLAfterModifierRuns() throws {
let sheet = shopifyCheckout.appearance(.storefront)
func testAppearanceDecoratesCheckoutURLFromCapturedConfiguration() throws {
let sheet = shopifyCheckout.appearance(.app(.dark))
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")
XCTAssertEqual(items.first(where: { $0.name == "ec_color_scheme" })?.value, "dark")
XCTAssertEqual(items.first(where: { $0.name == "ck_branding" })?.value, "app")
}

func testTintColor() {
func testTintColorIsCapturedWithoutChangingGlobalConfiguration() {
let globalColor = ShopifyCheckoutKit.configuration.tintColor
let color = UIColor.blue
shopifyCheckout.tintColor(color)
XCTAssertEqual(ShopifyCheckoutKit.configuration.tintColor, color)

let sheet = shopifyCheckout.tintColor(color)

XCTAssertEqual(sheet.configuration.tintColor, color)
XCTAssertEqual(shopifyCheckout.configuration.tintColor, globalColor)
XCTAssertEqual(ShopifyCheckoutKit.configuration.tintColor, globalColor)
}

func testTitle() {
func testTitleIsCapturedWithoutChangingGlobalConfiguration() {
let globalTitle = ShopifyCheckoutKit.configuration.title
let title = "Test Title"
shopifyCheckout.title(title)
XCTAssertEqual(ShopifyCheckoutKit.configuration.title, title)

let sheet = shopifyCheckout.title(title)

XCTAssertEqual(sheet.configuration.title, title)
XCTAssertEqual(shopifyCheckout.configuration.title, globalTitle)
XCTAssertEqual(ShopifyCheckoutKit.configuration.title, globalTitle)
}

func testCloseButtonTintColor() {
func testCloseButtonTintColorIsCapturedWithoutChangingGlobalConfiguration() {
let globalColor = ShopifyCheckoutKit.configuration.closeButtonTintColor
let color = UIColor.green
shopifyCheckout.closeButtonTintColor(color)
XCTAssertEqual(ShopifyCheckoutKit.configuration.closeButtonTintColor, color)

let sheet = shopifyCheckout.closeButtonTintColor(color)

XCTAssertEqual(sheet.configuration.closeButtonTintColor, color)
XCTAssertEqual(shopifyCheckout.configuration.closeButtonTintColor, globalColor)
XCTAssertEqual(ShopifyCheckoutKit.configuration.closeButtonTintColor, globalColor)
}

func testCloseButtonTintColorNil() {
shopifyCheckout.closeButtonTintColor(nil)
func testCloseButtonTintColorCanBeClearedOnInstance() {
let sheet = shopifyCheckout
.closeButtonTintColor(.green)
.closeButtonTintColor(nil)

XCTAssertNil(sheet.configuration.closeButtonTintColor)
XCTAssertNil(ShopifyCheckoutKit.configuration.closeButtonTintColor)
}

func testModifiersDoNotInvalidatePreload() async {
await Task.yield()
ShopifyCheckoutKit.preload(checkout: checkoutURL)
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())

_ = shopifyCheckout
.backgroundColor(.red)
.appearance(.app(.dark))
.tintColor(.blue)
.title("Instance checkout")
.closeButtonTintColor(.green)
await Task.yield()

XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())
}
}
Loading