Skip to content
Open
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
5 changes: 5 additions & 0 deletions platforms/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ Clear unused preloaded checkout work with `invalidate`:
ShopifyCheckoutKit.invalidate()
```

Updating configuration through `ShopifyCheckoutKit.configure {}` triggers
`ShopifyCheckoutKit.invalidate()`. Discarding cached preloads avoids presenting a
stale checkout. If a preload may already have run when configuration changes,
call `preload` again.

Preloading is enabled by default. Disable it when appropriate, for example for data-saver modes or app-specific runtime conditions:

```kotlin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public object ShopifyCheckoutKit {
/**
* Allows configuring ShopifyCheckoutKit.
*
* Calling this function invalidates any cached preload.
*
* Kotlin example:
* {@code ShopifyCheckoutKit.configure { it.appearance = CheckoutAppearance.App(ColorScheme.Dark()) }}
*
Expand Down
10 changes: 9 additions & 1 deletion platforms/react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,15 @@ Instead, a better approach is to call `preload()` when you have a strong enough

### Cache invalidation

Should you wish to manually clear the preload cache, call `invalidate()` on your `ShopifyCheckout` instance or the value returned by `useShopifyCheckout()`.
To manually clear the preload cache, call `invalidate()` on your `ShopifyCheckout`
instance or the value returned by `useShopifyCheckout()`.

Updating configuration through `shopifyCheckout.setConfig(...)` or the
`configuration` prop on `ShopifyCheckoutProvider` triggers
`shopifyCheckout.invalidate()`. Discarding cached preloads avoids presenting a
stale checkout. If a preload may already have run when configuration changes,
call `preload` again. Keep a provider's configuration object stable across renders to
avoid unintentionally invalidating preloads.

## Checkout lifecycle

Expand Down
8 changes: 7 additions & 1 deletion platforms/swift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ Clear unused preloaded checkout work with `invalidate`:
ShopifyCheckoutKit.invalidate()
```

Updating configuration through `ShopifyCheckoutKit.configure {}` or a direct
mutation such as `ShopifyCheckoutKit.configuration.title = "Checkout"` triggers
`ShopifyCheckoutKit.invalidate()`. Discarding cached preloads avoids presenting a
stale checkout. If a preload may already have run when configuration changes,
call `preload` again.

Preloading is enabled by default. Disable it when appropriate, for example for data-saver modes or app-specific runtime conditions:

```swift
Expand All @@ -250,7 +256,7 @@ 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.
`ShopifyCheckout` uses the global configuration as its defaults. When present, modifiers such as `.appearance(...)`, `.tintColor(...)`, and `.title(...)` take precedence over the corresponding `ShopifyCheckoutKit.configuration` values for that checkout.

| Option | Default | Purpose |
| --- | --- | --- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ public class CheckoutViewController: UINavigationController {
public init(checkout url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) {
let rootViewController = CheckoutWebViewController(
checkoutURL: url,
configuration: ShopifyCheckoutKit.configuration,
delegate: delegate,
client: client,
entryPoint: nil
Expand All @@ -22,7 +21,6 @@ public class CheckoutViewController: UINavigationController {
package init(checkout url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) {
let rootViewController = CheckoutWebViewController(
checkoutURL: url,
configuration: ShopifyCheckoutKit.configuration,
delegate: delegate,
client: client,
entryPoint: entryPoint
Expand Down Expand Up @@ -65,21 +63,28 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
public typealias UIViewControllerType = CheckoutViewController

var checkoutURL: URL
var configuration: Configuration
var configurationModifiers: [(inout Configuration) -> Void] = []
var client: (any CheckoutCommunicationProtocol)?
var onDismissAction: (() -> Void)?
var onFailAction: ((CheckoutError) -> Void)?

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

var configuration: Configuration {
var configuration = ShopifyCheckoutKit.configuration
configurationModifiers.forEach { $0(&configuration) }
return configuration
}

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

public func makeUIViewController(context _: Self.Context) -> CheckoutViewController {
let configuration = configuration
let decoratedCheckoutURL = CheckoutURLDecorator.decorate(checkoutURL, configuration: configuration)
let viewController = CheckoutViewController(
checkout: decoratedCheckoutURL,
configuration: configuration,
Expand All @@ -93,7 +98,7 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
configureWebViewController(uiViewController)
}

private func configureWebViewController(_ navigationController: CheckoutViewController) {
func configureWebViewController(_ navigationController: CheckoutViewController) {
guard
let webViewController = navigationController
.viewControllers
Expand All @@ -103,6 +108,7 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab
return
}

webViewController.apply(configuration: configuration)
webViewController.client = client
webViewController.checkoutView?.client = client
webViewController.onDismiss = onDismissAction
Expand Down Expand Up @@ -161,12 +167,12 @@ extension CheckoutConfigurable {
modifyingConfiguration { $0.closeButtonTintColor = color }
}

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

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

lazy var progressBar: ProgressBarView = {
let progressBar = ProgressBarView(frame: .zero, tintColor: configuration.tintColor)
let progressBar = ProgressBarView(frame: .zero, tintColor: resolvedConfiguration.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 = configuration.closeButtonTintColor {
var item: UIBarButtonItem

if #available(iOS 26.0, *) {
item = UIBarButtonItem(
image: UIImage(systemName: "xmark"),
style: .plain,
target: self,
action: #selector(close)
)
} else {
item = UIBarButtonItem(
image: UIImage(systemName: "xmark.circle.fill"),
style: .plain,
target: self,
action: #selector(close)
)
}
private var configurationOverride: Configuration?

item.tintColor = closeButtonTintColor
item.accessibilityIdentifier = Self.closeButtonAccessibilityIdentifier
return item
}

let item = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(close)
)
item.accessibilityIdentifier = Self.closeButtonAccessibilityIdentifier
return item
}()
private var resolvedConfiguration: Configuration {
configurationOverride ?? ShopifyCheckoutKit.configuration
}

var progressObserver: NSKeyValueObservation?

// MARK: Initializers

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

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

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

checkoutView.viewDelegate = self
applyPresentationConfiguration(resolvedConfiguration)
}

func apply(configuration: Configuration) {
configurationOverride = configuration
applyPresentationConfiguration(configuration)
}

private func applyPresentationConfiguration(_ configuration: Configuration) {
title = configuration.title
view.backgroundColor = configuration.backgroundColor
checkoutView?.backgroundColor = configuration.backgroundColor
checkoutView?.underPageBackgroundColor = configuration.backgroundColor
progressBar.progressBar.tintColor = configuration.tintColor
navigationItem.rightBarButtonItem = closeBarButtonItem(tintColor: configuration.closeButtonTintColor)
}

navigationItem.rightBarButtonItem = closeBarButtonItem
private func closeBarButtonItem(tintColor: UIColor?) -> UIBarButtonItem {
guard let tintColor else {
let item = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(close)
)
item.accessibilityIdentifier = Self.closeButtonAccessibilityIdentifier
return item
}

checkoutView.viewDelegate = self
let item: UIBarButtonItem
if #available(iOS 26.0, *) {
item = UIBarButtonItem(
image: UIImage(systemName: "xmark"),
style: .plain,
target: self,
action: #selector(close)
)
} else {
item = UIBarButtonItem(
image: UIImage(systemName: "xmark.circle.fill"),
style: .plain,
target: self,
action: #selector(close)
)
}

view.backgroundColor = configuration.backgroundColor
item.tintColor = tintColor
item.accessibilityIdentifier = Self.closeButtonAccessibilityIdentifier
return item
}

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

view.backgroundColor = configuration.backgroundColor
applyPresentationConfiguration(resolvedConfiguration)
}

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

private var progressAnimation: UIViewPropertyAnimator?
private let configuredTintColor: UIColor

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

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

progressBar.tintColor = configuredTintColor
progressBar.tintColor = tintColor
}

override func didMoveToSuperview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,29 @@ public let version = "4.0.0-alpha.5"
private let lockedCheckoutKitConfiguration = LockedValue(Configuration())

/// The configuration options for the `ShopifyCheckoutKit` library.
///
/// Assigning configuration invalidates any cached preload.
public var configuration: Configuration {
get { lockedCheckoutKitConfiguration.get() }
set {
let previousConfiguration = lockedCheckoutKitConfiguration.get()
lockedCheckoutKitConfiguration.set(newValue)
applyConfigurationChange(
configuration: newValue,
previousConfiguration: previousConfiguration
)
applyConfigurationChange(newValue)
}
}

/// A convienence function for configuring the `ShopifyCheckoutKit` library.
/// A convenience function for configuring the `ShopifyCheckoutKit` library.
///
/// Calling this function invalidates any cached preload.
public func configure(_ block: (inout Configuration) -> Void) {
let previousConfiguration = lockedCheckoutKitConfiguration.get()
lockedCheckoutKitConfiguration.update(block)
applyConfigurationChange(
configuration: lockedCheckoutKitConfiguration.get(),
previousConfiguration: previousConfiguration
)
applyConfigurationChange(lockedCheckoutKitConfiguration.get())
}

private func applyConfigurationChange(configuration: Configuration, previousConfiguration: Configuration) {
private func applyConfigurationChange(_ configuration: Configuration) {
OSLogger.shared.logLevel = configuration.logLevel

if configuration.preloading.enabled != previousConfiguration.preloading.enabled {
Task { @MainActor in
invalidate()
}
Task { @MainActor in
invalidate()
}
}

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

func testViewWillAppearAppliesLatestGlobalConfigurationToImperativeCheckout() {
ShopifyCheckoutKit.configuration.title = "Updated global title"

viewController.viewWillAppear(false)

XCTAssertEqual(viewController.title, "Updated global title")
}

func testInstanceConfigurationIsAppliedToCheckoutChrome() throws {
var configuration = Configuration()
configuration.backgroundColor = .red
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,22 @@ class ConfigurationTests: XCTestCase {
XCTAssertFalse(CheckoutWebView.preloadCache.hasEntry())
}

func testChangingConfigurationWithoutChangingPreloadingDoesNotInvalidatePreload() async throws {
func testChangingConfigurationWithoutChangingPreloadingInvalidatesPreload() async throws {
let checkoutURL = try XCTUnwrap(URL(string: "https://shopify1.shopify.com/checkouts/cn/123"))

await Task.yield()
ShopifyCheckoutKit.preload(checkout: checkoutURL)
XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry())

ShopifyCheckoutKit.configure {
$0.title = "Thank you!"
}

for _ in 0 ..< 10 {
for _ in 0 ..< 10 where CheckoutWebView.preloadCache.hasEntry() {
await Task.yield()
}

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

func testAppearanceCanBeSetDirectly() {
Expand Down
Loading
Loading