From c87f4d66d6de2f7f3cd7542b0790eaa9c1594e36 Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Tue, 15 Sep 2026 17:16:50 +0200 Subject: [PATCH] Document and test preload reuse with instance appearance Evaluating SwiftUI modifiers preserves a cached preload, but reuse is decided at presentation: preload decorates the checkout URL with the global appearance, so an instance .appearance(...) that differs from it produces a different decorated URL, which misses and evicts the preload. The README now separates preservation from reuse, and presentation-level tests cover both reuse with matching appearance plus instance-specific chrome modifiers and the miss-and-evict path for a differing appearance. Co-Authored-By: Claude Fable 5 Assisted-By: devx/b61614bc-75f6-4454-beb1-4d30f281e365 --- platforms/swift/README.md | 4 +- .../SwiftUITests.swift | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/platforms/swift/README.md b/platforms/swift/README.md index 768cbdbd5..cad39b69d 100644 --- a/platforms/swift/README.md +++ b/platforms/swift/README.md @@ -218,7 +218,7 @@ ShopifyCheckoutKit.present( ) ``` -Preloading is a best-effort performance hint, not a guarantee. If the preload is unavailable, incomplete, or for a different checkout URL, checkout loads normally during presentation. A preloaded checkout reflects the cart represented by the URL passed to `preload`, so call `preload` again after cart changes produce a new checkout URL. +Preloading is a best-effort performance hint, not a guarantee. If the preload is unavailable, incomplete, or for a different checkout URL, checkout loads normally during presentation. A preloaded checkout reflects the cart represented by the URL passed to `preload`, so call `preload` again after cart changes produce a new checkout URL. It also reflects the `appearance` in `ShopifyCheckoutKit.configuration` at the time of the `preload` call: in SwiftUI, keep any instance `.appearance(...)` modifier aligned with the global appearance when relying on preloading, since a mismatch skips and clears the preload at presentation. Avoid preloading on every add-to-cart or cart mutation. Preload only when buyer intent is strong enough to justify the additional client and network work. @@ -259,7 +259,7 @@ ShopifyCheckoutKit.configure { } ``` -`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. +`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. Applying a modifier does not mutate `ShopifyCheckoutKit.configuration`, so it does not trigger the preload invalidation described above. Whether the preload is reused is decided later, at presentation: `preload` prepares the checkout URL using the global configuration's `appearance`, so a `ShopifyCheckout` reuses the preload only when its effective appearance matches the one that was preloaded. Presenting with a different instance `.appearance(...)` loads checkout fresh and discards the preloaded checkout. `.tintColor(...)`, `.backgroundColor(...)`, `.title(...)`, and `.closeButtonTintColor(...)` do not affect the checkout URL and never affect preload reuse. | Option | Default | Purpose | | --- | --- | --- | diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/SwiftUITests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/SwiftUITests.swift index b95e18107..332681ab9 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/SwiftUITests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/SwiftUITests.swift @@ -74,12 +74,14 @@ class CheckoutConfigurableTests: XCTestCase { override func setUp() async throws { try await super.setUp() ShopifyCheckoutKit.configuration = Configuration() + CheckoutWebView.invalidate() checkoutURL = URL(string: "https://www.shopify.com") shopifyCheckout = ShopifyCheckout(checkout: checkoutURL) } override func tearDown() async throws { ShopifyCheckoutKit.configuration = Configuration() + CheckoutWebView.invalidate() try await super.tearDown() } @@ -171,6 +173,41 @@ class CheckoutConfigurableTests: XCTestCase { XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry()) } + func testPresentationReusesPreloadWhenInstanceAppearanceMatchesPreloadedAppearance() async { + await Task.yield() + ShopifyCheckoutKit.preload(checkout: checkoutURL) + CheckoutWebView.preloadCache.transition(to: .ready) + let preloaded = CheckoutWebView.for(checkout: CheckoutURLDecorator.decorate(checkoutURL)) + + let sheet = shopifyCheckout + .backgroundColor(.red) + .tintColor(.blue) + .title("Instance checkout") + .closeButtonTintColor(.green) + + XCTAssertEqual(sheet.decoratedCheckoutURL, CheckoutURLDecorator.decorate(checkoutURL)) + let presented = CheckoutWebView.for(checkout: sheet.decoratedCheckoutURL) + + XCTAssertTrue(presented === preloaded) + XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry()) + } + + func testPresentationWithDifferentInstanceAppearanceMissesAndEvictsPreload() async { + await Task.yield() + ShopifyCheckoutKit.preload(checkout: checkoutURL) + CheckoutWebView.preloadCache.transition(to: .ready) + XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry()) + + let sheet = shopifyCheckout.appearance(.app(.dark)) + XCTAssertNotEqual(sheet.decoratedCheckoutURL, CheckoutURLDecorator.decorate(checkoutURL)) + + let fresh = CheckoutWebView.for(checkout: sheet.decoratedCheckoutURL) + + XCTAssertNil(fresh.url) + XCTAssertFalse(CheckoutWebView.preloadCache.hasEntry()) + XCTAssertFalse(CheckoutWebView.preloadCache.hasActiveKeepAlive()) + } + func testModifiersApplyToAnyConformerWithoutCasts() { let fixture = ConfigurableFixture() .backgroundColor(.red)