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
63 changes: 50 additions & 13 deletions platforms/swift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,7 @@ struct CartView: View {
.sheet(isPresented: $isPresented) {
ShopifyCheckout(checkout: checkoutURL)
.title("Checkout")
.appearance(.storefront)
.tintColor(.systemBlue)
.backgroundColor(.systemBackground)
.closeButtonTintColor(nil)
.appearance(.storefront(colors: Colors(progressIndicator: .systemBlue)))
.onDismiss {
isPresented = false
}
Expand Down Expand Up @@ -245,24 +242,18 @@ Configure global presentation defaults before presenting checkout. `ShopifyCheck
import ShopifyCheckoutKit

ShopifyCheckoutKit.configure {
$0.appearance = .storefront
$0.tintColor = .systemBlue
$0.backgroundColor = .systemBackground
$0.closeButtonTintColor = nil
$0.appearance = .storefront(colors: Colors(progressIndicator: .systemBlue))
$0.logLevel = .debug
$0.telemetry.enabled = false
}
```

`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. Modifiers do not mutate `ShopifyCheckoutKit.configuration`, so they do not invalidate a cached preload.
`ShopifyCheckout` uses the global configuration as its defaults. When present, modifiers such as `.appearance(...)` and `.title(...)` take precedence over the corresponding `ShopifyCheckoutKit.configuration` values for that checkout. Modifiers do not mutate `ShopifyCheckoutKit.configuration`, so they do not 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)`. |
| `tintColor` | Shopify blue | Progress indicator color while checkout initializes. |
| `backgroundColor` | `.systemBackground` | Background behind the web view while checkout initializes. |
| `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())`. Each appearance owns its native colors. |
| `title` | Localized `shopify_checkout_kit_title` or `Checkout` | Navigation title for the checkout sheet. |
| `closeButtonTintColor` | `nil` | Optional tint for the close button. |
| `logLevel` | `.warn` | SDK logging verbosity. Threshold-ordered `.debug` → `.warn` → `.error` → `.none`; use `.debug` during integration. |
| `preloading.enabled` | `true` | Enables best-effort checkout preloading before presentation. |
| `allowedMessageOrigins` | `[]` | Origins trusted to send incoming checkout messages. Empty trusts every origin (open by default). See [Incoming message origin validation](#incoming-message-origin-validation). |
Expand All @@ -277,6 +268,52 @@ operating system for delivery.

To localize the title, add `shopify_checkout_kit_title` to your app's `Localizable.xcstrings`.

### Appearance and native colors

`CheckoutAppearance` owns the native palette. Use `.storefront(colors:)` to customize the surrounding native UI without changing the merchant's web checkout branding, or `.app(...)` with a `ColorScheme` to use Checkout Kit branding.

`Colors` uses native `UIColor` and `UIImage` values. Dynamic colors, accessibility contrast, and transparency remain supported. The default navigation bar remains transparent and the default close button remains the iOS system control.

| `Colors` property | Default | Purpose |
| --- | --- | --- |
| `webViewBackground` | `.systemBackground` | Background behind the WebView and its overscroll area. |
| `headerBackground` | `.clear` | Navigation-bar background. |
| `headerFont` | `.label` | Navigation-title color. |
| `progressIndicator` | Shopify blue | Progress indicator while checkout loads. |
| `closeIcon` | `nil` | Optional native image for the close button. |
| `closeIconTint` | `nil` | Optional close-button tint. With no icon or tint override, iOS supplies the system close button. |
| `headerBorderColor` | `nil` | Optional navigation-bar shadow color. |

Light and dark schemes each own one palette. Automatic appearance owns independent `lightColors` and `darkColors`, and switches natively when the system appearance changes:

```swift
ShopifyCheckoutKit.configure {
$0.appearance = .app(.automatic(
lightColors: Colors(progressIndicator: .systemBlue, closeIconTint: .systemBlue),
darkColors: Colors(progressIndicator: .systemTeal, closeIconTint: .systemTeal)
))
}
```

Use `customize` to derive a modified copy without changing the original palette or scheme. A single customization applies to both automatic palettes; use `customize(light:dark:)` for separate automatic overrides:

```swift
let appearance = CheckoutAppearance.app(
.automatic().customize(
light: { $0.progressIndicator = .systemBlue },
dark: { $0.progressIndicator = .systemTeal }
)
)

ShopifyCheckoutKit.configure {
$0.appearance = appearance
}
```

Set appearance before presenting checkout. Replacing it with `.storefront()`, `.app(.light())`, `.app(.dark())`, or `.app(.automatic())` restores that appearance's default colors. Updating unrelated settings such as logging, preloading, or the checkout title preserves the configured palette. A presented checkout uses the native colors of the configuration it was created with, including a reused preloaded WebView.

The previous configuration-level `backgroundColor`, `tintColor`, `spinnerColor`, and `closeButtonTintColor` properties, and the corresponding SwiftUI color modifiers, have been removed. Use `Colors.webViewBackground`, `Colors.progressIndicator`, and `Colors.closeIconTint` through `appearance` instead. `CheckoutAppearance` replaces `Configuration.Appearance`, and `ShopifyCheckoutKit.ColorScheme` replaces `Configuration.ColorScheme`; qualify the latter when also importing `SwiftUI`.

### Incoming message origin validation

The native web view is a private, app-controlled runtime, so Checkout Kit is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
) as? Bool ?? true

ShopifyCheckoutKit.configure {
$0.appearance = .app(.automatic)
$0.tintColor = ColorPalette.primaryColor
$0.appearance = .app(.automatic().customize {
$0.progressIndicator = ColorPalette.primaryColor
})
$0.logger = ObservingLogger(wrapping: FileLogger("log.txt"))
$0.logLevel = checkoutKitLogLevel
$0.preloading.enabled = checkoutPreloadingEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

setupControllers()
subscribeToCartUpdates()
subscribeToColorSchemeChanges()
subscribeToAppearanceChanges()

var viewControllers: [UIViewController?] = Array(repeating: nil, count: Screen.allCases.count)

Expand Down Expand Up @@ -70,9 +70,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
self.window = window
}

private func subscribeToColorSchemeChanges() {
// Subscribe to color scheme changes on the settings screen
NotificationCenter.default.addObserver(self, selector: #selector(colorSchemeChanged), name: .colorSchemeChanged, object: nil)
private func subscribeToAppearanceChanges() {
NotificationCenter.default.addObserver(self, selector: #selector(appearanceChanged), name: .appearanceChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(navigateToAccountTab), name: .navigateToAccount, object: nil)
}

Expand Down Expand Up @@ -299,7 +298,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
navigateTo(.catalog)
}

@objc func colorSchemeChanged() {
@objc func appearanceChanged() {
window?.overrideUserInterfaceStyle = ShopifyCheckoutKit.configuration.appearance.userInterfaceStyle
}

Expand All @@ -316,11 +315,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}

extension Notification.Name {
static let colorSchemeChanged = Notification.Name("colorSchemeChanged")
static let appearanceChanged = Notification.Name("appearanceChanged")
static let navigateToAccount = Notification.Name("navigateToAccount")
}

extension Configuration.ColorScheme {
extension ShopifyCheckoutKit.ColorScheme {
var userInterfaceStyle: UIUserInterfaceStyle {
switch self {
case .light:
Expand All @@ -333,13 +332,13 @@ extension Configuration.ColorScheme {
}
}

extension Configuration.Appearance {
extension CheckoutAppearance {
var userInterfaceStyle: UIUserInterfaceStyle {
switch self {
case let .app(colorScheme):
return colorScheme.userInterfaceStyle
case .storefront:
return Configuration.ColorScheme.light.userInterfaceStyle
return .light
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ struct CartView: View {
.sheet(isPresented: $showCheckoutSheet) {
if let url = cartManager.cart?.checkoutURL {
ShopifyCheckout(checkout: url)
.appearance(.app(.automatic))
.title("Checkout (SwiftUI)")
.onStart { event in
print("[CheckoutKitSwiftDemo] Started: \(event.checkout.id)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ struct SettingsView: View {
var windowOpenHandler: WindowOpenHandlerOption = .default

@State private var logs: [String?] = LogReader.shared.readLogs() ?? []
@State private var selectedAppearance = ShopifyCheckoutKit.configuration.appearance
@State private var selectedAppearanceOption = AppearanceOption(appearance: ShopifyCheckoutKit.configuration.appearance)
@State private var isResettingSession = false
@State private var showingSessionReset = false

Expand Down Expand Up @@ -134,19 +134,17 @@ struct SettingsView: View {
ForEach(AppearanceOption.allCases) { option in
AppearanceOptionView(
title: option.title,
isSelected: option.appearance == selectedAppearance
isSelected: option == selectedAppearanceOption
)
.background(Color.clear)
.contentShape(Rectangle())
.onTapGesture {
selectedAppearance = option.appearance
selectedAppearanceOption = option
ShopifyCheckoutKit.configure {
$0.appearance = option.appearance
$0.tintColor = option.appearance.colorScheme.tintColor
$0.backgroundColor = option.appearance.colorScheme.backgroundColor
}
NotificationCenter.default.post(
name: .colorSchemeChanged, object: nil
name: .appearanceChanged, object: nil
)
}
}
Expand Down Expand Up @@ -306,6 +304,19 @@ enum AppearanceOption: CaseIterable, Identifiable {
case appLight
case appDark

init(appearance: CheckoutAppearance) {
switch appearance {
case .storefront:
self = .storefront
case .app(.automatic):
self = .appAutomatic
case .app(.light):
self = .appLight
case .app(.dark):
self = .appDark
}
}

var id: Self {
self
}
Expand All @@ -323,37 +334,16 @@ enum AppearanceOption: CaseIterable, Identifiable {
}
}

var appearance: Configuration.Appearance {
var appearance: CheckoutAppearance {
switch self {
case .storefront:
return .storefront
return .storefront()
case .appAutomatic:
return .app(.automatic)
return .app(.automatic())
case .appLight:
return .app(.light)
return .app(.light())
case .appDark:
return .app(.dark)
}
}
}

extension Configuration.ColorScheme {
var tintColor: UIColor {
return UIColor(red: 0.09, green: 0.45, blue: 0.69, alpha: 1.00)
}

var backgroundColor: UIColor {
return .systemBackground
}
}

extension Configuration.Appearance {
var colorScheme: Configuration.ColorScheme {
switch self {
case let .app(colorScheme):
return colorScheme
case .storefront:
return .light
return .app(.dark())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@testable import CheckoutKitSwiftDemo
import ShopifyCheckoutKit
import XCTest

@MainActor
final class AppearanceOptionTests: XCTestCase {
func testDefaultAppearancesSelectTheMatchingOption() {
for (appearance, option) in defaultAppearances() {
XCTAssertEqual(AppearanceOption(appearance: appearance), option)
}
}

func testCustomNativeColorsDoNotChangeTheSelectedAppearanceOption() {
let colors = Colors(webViewBackground: .red, progressIndicator: .green, closeIconTint: .blue)
let appearances: [(CheckoutAppearance, AppearanceOption)] = [
(.storefront(colors: colors), .storefront),
(.app(.light(colors: colors)), .appLight),
(.app(.dark(colors: colors)), .appDark),
(.app(.automatic(lightColors: colors, darkColors: Colors(progressIndicator: .purple))), .appAutomatic)
]

for (appearance, option) in appearances {
XCTAssertEqual(AppearanceOption(appearance: appearance), option)
}
}

func testAppearanceOptionsCreateFreshDefaultPalettes() {
for (appearance, option) in defaultAppearances() {
XCTAssertEqual(option.appearance, appearance)
}
}

private func defaultAppearances() -> [(CheckoutAppearance, AppearanceOption)] {
[
(.storefront(), .storefront),
(.app(.automatic()), .appAutomatic),
(.app(.light()), .appLight),
(.app(.dark()), .appDark)
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ struct Internal_ApplePayButton: View {
private let cornerRadius: CGFloat?
@Environment(\.colorScheme) private var colorScheme

func buttonIdentity(colorScheme: ColorScheme) -> String {
func buttonIdentity(colorScheme: SwiftUI.ColorScheme) -> String {
return "\(colorScheme)-\(buttonType.rawValue)-\(buttonStyle.rawValue)"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
public enum CheckoutAppearance: Equatable, Sendable {
case app(ColorScheme = .automatic())
case storefront(colors: Colors = .init())

var effectiveColorScheme: ColorScheme {
switch self {
case let .app(colorScheme):
return colorScheme
case let .storefront(colors):
return .light(colors: colors)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,9 @@ enum CheckoutURLDecorator {
private static let brandingQueryItemName = "ck_branding"
}

extension Configuration.Appearance {
extension CheckoutAppearance {
fileprivate var colorSchemeValue: String {
switch self {
case let .app(colorScheme):
return colorScheme.rawValue
case .storefront:
return Configuration.ColorScheme.light.rawValue
}
effectiveColorScheme.id
}

fileprivate var brandingValue: String {
Expand Down
Loading
Loading