From cdfaea2356baa809d35cc15f883c977d4bb654b5 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Mon, 24 Aug 2026 09:10:38 +0100 Subject: [PATCH 1/3] [Swift] Prefetch wallet settings and add loading skeletons --- platforms/swift/README.md | 19 +- .../ShopifyAcceleratedCheckoutsApp.swift | 2 +- .../Views/Components/ButtonSet.swift | 7 - .../StorefrontAPI/StorefrontAPI+Queries.swift | 57 ++- ...fyAcceleratedCheckouts+Configuration.swift | 55 +++ .../Wallets/AcceleratedCheckoutButtons.swift | 75 ++-- .../Wallets/Wallet.swift | 20 +- .../Wallets/WalletButtonSkeleton.swift | 26 ++ .../StorefrontAPI/QueryCacheTests.swift | 145 ++++++++ ...celeratedCheckoutButtonsLoadingTests.swift | 46 +++ .../api/ShopifyAcceleratedCheckouts.json | 343 ++++++++++++++++++ 11 files changed, 749 insertions(+), 46 deletions(-) create mode 100644 platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/WalletButtonSkeleton.swift create mode 100644 platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/QueryCacheTests.swift create mode 100644 platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/AcceleratedCheckoutButtonsLoadingTests.swift diff --git a/platforms/swift/README.md b/platforms/swift/README.md index 53e7a0d2c..da02d2bc1 100644 --- a/platforms/swift/README.md +++ b/platforms/swift/README.md @@ -461,7 +461,7 @@ iOS handles checkout geolocation permission prompts through the system prompt. I ### Configure accelerated checkouts -Create shared configuration values and inject them into your SwiftUI hierarchy: +Create shared configuration values and apply them to your SwiftUI hierarchy: ```swift import ShopifyAcceleratedCheckouts @@ -484,13 +484,19 @@ struct YourApp: App { var body: some Scene { WindowGroup { ContentView() - .environment(\.shopifyAcceleratedCheckoutsConfiguration, checkoutConfig) + .shopifyAcceleratedCheckouts(checkoutConfig) .environment(\.shopifyApplePayConfiguration, applePayConfig) } } } ``` +Apply `.shopifyAcceleratedCheckouts(_:)` as soon as the shop configuration is available. The +modifier preserves the configuration in the SwiftUI environment and automatically prefetches the +shop settings used by the wallet buttons. Requests are deduplicated across button instances; +settings remain fresh for one hour and stale settings are served while they refresh in the +background. Existing direct environment injection remains supported. + Use one customer mode at a time: ```swift @@ -528,6 +534,15 @@ AcceleratedCheckoutButtons(cartID: cartID) .connect(client) ``` +While shop settings load, the SDK renders neutral placeholders matching the height, spacing, +count, and corner radius of the configured wallet buttons. The placeholders respect Reduce Motion +and are hidden from accessibility. To provide your own loading UI, disable the SDK presentation: + +```swift +AcceleratedCheckoutButtons(cartID: cartID) + .loadingPresentation(.hidden) +``` + You can also render buttons for a single product variant: ```swift diff --git a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift index 8c4f0061f..3b93e9b02 100644 --- a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift +++ b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.swift @@ -51,7 +51,7 @@ struct ShopifyAcceleratedCheckoutsApp: App { ShopifyAcceleratedCheckouts.logLevel = logLevel updateConfiguration() } - .environment(\.shopifyAcceleratedCheckoutsConfiguration, configuration) + .shopifyAcceleratedCheckouts(configuration) .environment(\.shopifyApplePayConfiguration, applePayConfiguration) } .environment(\.locale, Locale(identifier: locale)) diff --git a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift index df2bf6ce5..adc250d7b 100644 --- a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift +++ b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp/Views/Components/ButtonSet.swift @@ -77,13 +77,6 @@ private struct CheckoutSection: View { .foregroundColor(.primary) .multilineTextAlignment(.center) - if case .loading = renderState { - VStack(spacing: 12) { - SkeletonButton(cornerRadius: 8) - SkeletonButton(cornerRadius: 8) - } - } - if case .error = renderState { VStack { Image(systemName: "exclamationmark.triangle") diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift index 7a707088d..a6bc38661 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift @@ -42,20 +42,37 @@ extension StorefrontAPI { actor QueryCache { static let shared = QueryCache() - private var cache: [String: any Sendable] = [:] + static let defaultFreshnessInterval: TimeInterval = 60 * 60 + + private struct CacheEntry { + let value: any Sendable + let cachedAt: Date + } + + private let freshnessInterval: TimeInterval + private var cache: [String: CacheEntry] = [:] private var inflightRequests: [String: any Sendable] = [:] - private init() {} + init(freshnessInterval: TimeInterval = defaultFreshnessInterval) { + self.freshnessInterval = freshnessInterval + } - /// Loads data with deduplication - multiple simultaneous calls will share the same request + /// Loads data with deduplication and stale-while-revalidate caching. + /// + /// Fresh values are returned from memory. Stale values are returned immediately while + /// one shared refresh runs in the background. Failed refreshes leave the stale value intact. func load( cacheKey: String, url: URL, + date: Date = Date(), query: @Sendable @escaping () async throws -> T ) async throws -> T { let key = buildCacheKey(queryKey: cacheKey, url: url) - if let cached = cache[key] as? T { + if let entry = cache[key], let cached = entry.value as? T { + if date.timeIntervalSince(entry.cachedAt) >= freshnessInterval { + refreshIfNeeded(key: key, date: date, query: query) + } return cached } @@ -64,15 +81,14 @@ actor QueryCache { } let task = Task { - let result = try await query() - self.cache(result, for: key) - return result + try await query() } inflightRequests[key] = task do { let result = try await task.value + cache(result, for: key, date: date) inflightRequests.removeValue(forKey: key) return result } catch { @@ -81,8 +97,31 @@ actor QueryCache { } } - private func cache(_ result: some Sendable, for key: String) { - cache[key] = result + private func refreshIfNeeded( + key: String, + date: Date, + query: @Sendable @escaping () async throws -> T + ) { + guard inflightRequests[key] == nil else { return } + + let task = Task { + try await query() + } + inflightRequests[key] = task + + Task { + do { + let result = try await task.value + cache(result, for: key, date: date) + } catch { + // Keep serving the stale value and retry on a future load. + } + inflightRequests.removeValue(forKey: key) + } + } + + private func cache(_ result: some Sendable, for key: String, date: Date) { + cache[key] = CacheEntry(value: result, cachedAt: date) } private func buildCacheKey(queryKey: String, url: URL) -> String { diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift index 9a3a085d9..610a9e43b 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift @@ -70,3 +70,58 @@ extension EnvironmentValues { set { self[ShopifyAcceleratedCheckoutsConfigurationKey.self] = newValue } } } + +@available(iOS 16.0, *) +private struct ShopifyAcceleratedCheckoutsConfigurationModifier: ViewModifier { + let configuration: ShopifyAcceleratedCheckouts.Configuration + + func body(content: Content) -> some View { + content + .environment(\.shopifyAcceleratedCheckoutsConfiguration, configuration) + .task(id: prefetchIdentity) { + await ShopSettingsPrefetcher.prefetch(configuration: configuration) + } + } + + private var prefetchIdentity: ShopSettingsPrefetchIdentity { + ShopSettingsPrefetchIdentity( + storefrontDomain: configuration.storefrontDomain, + storefrontAccessToken: configuration.storefrontAccessToken + ) + } +} + +@available(iOS 16.0, *) +private struct ShopSettingsPrefetchIdentity: Equatable { + let storefrontDomain: String + let storefrontAccessToken: String +} + +@available(iOS 16.0, *) +enum ShopSettingsPrefetcher { + static func prefetch(configuration: ShopifyAcceleratedCheckouts.Configuration) async { + let storefront = StorefrontAPI( + storefrontDomain: configuration.storefrontDomain, + storefrontAccessToken: configuration.storefrontAccessToken + ) + + do { + _ = try await storefront.shop() + } catch { + ShopifyAcceleratedCheckouts.logger.debug("Shop settings prefetch failed.") + } + } +} + +@available(iOS 16.0, *) +extension View { + /// Configures accelerated checkouts and prefetches shared shop settings. + /// + /// Apply this modifier to an ancestor view as soon as the shop configuration is known. + /// Accelerated checkout buttons reuse the prefetched result automatically. + public func shopifyAcceleratedCheckouts( + _ configuration: ShopifyAcceleratedCheckouts.Configuration + ) -> some View { + modifier(ShopifyAcceleratedCheckoutsConfigurationModifier(configuration: configuration)) + } +} diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift index a6417a42b..5c4bf3cfc 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift @@ -9,6 +9,18 @@ public enum RenderState: Equatable { case error(reason: String) } +@available(iOS 16.0, *) +extension ShopifyAcceleratedCheckouts { + /// Controls how accelerated checkout buttons represent their initial loading state. + public enum LoadingPresentation: Sendable, Equatable { + /// Show neutral placeholders that match the configured wallet button layout. + case automatic + + /// Render no loading UI. Use this when the containing app supplies its own loading state. + case hidden + } +} + /// Renders a Checkout buttons for a cart or product variant /// /// Note: @@ -25,6 +37,7 @@ public struct AcceleratedCheckoutButtons: View { var eventHandlers: EventHandlers = .init() var cornerRadius: CGFloat? var clientContainer: CheckoutProtocolClientContainer = .init() + var loadingPresentation: ShopifyAcceleratedCheckouts.LoadingPresentation = .automatic /// The Apple Pay button type private var applePayButtonType: PKPaymentButtonType = .plain @@ -61,30 +74,33 @@ public struct AcceleratedCheckoutButtons: View { } public var body: some View { - VStack { + VStack(spacing: WalletButtonLayout.spacing) { if let shopSettings { - VStack { - ForEach(wallets, id: \.self) { - switch $0 { - case .applePay: - ApplePayButton( - identifier: identifier, - eventHandlers: eventHandlers, - cornerRadius: cornerRadius, - buttonType: applePayButtonType, - buttonStyle: applePayButtonStyle, - client: clientContainer.client - ) - case .shopPay: - ShopPayButton( - identifier: identifier, - eventHandlers: eventHandlers, - cornerRadius: cornerRadius, - client: clientContainer.client - ) - } + ForEach(wallets, id: \.self) { + switch $0 { + case .applePay: + ApplePayButton( + identifier: identifier, + eventHandlers: eventHandlers, + cornerRadius: cornerRadius, + buttonType: applePayButtonType, + buttonStyle: applePayButtonStyle, + client: clientContainer.client + ) + case .shopPay: + ShopPayButton( + identifier: identifier, + eventHandlers: eventHandlers, + cornerRadius: cornerRadius, + client: clientContainer.client + ) } - }.environmentObject(shopSettings) + } + .environmentObject(shopSettings) + } else if currentRenderState == .loading, loadingPresentation == .automatic { + ForEach(wallets, id: \.self) { _ in + WalletButtonSkeleton(cornerRadius: cornerRadius) + } } } .task { await loadShopSettings() } @@ -95,7 +111,7 @@ public struct AcceleratedCheckoutButtons: View { private var resolvedConfiguration: ShopifyAcceleratedCheckouts.Configuration { guard let configuration else { - fatalError("Missing ShopifyAcceleratedCheckouts.Configuration. Add .environment(\\.shopifyAcceleratedCheckoutsConfiguration, ...) to an ancestor view.") + fatalError("Missing ShopifyAcceleratedCheckouts.Configuration. Add .shopifyAcceleratedCheckouts(...) or .environment(\\.shopifyAcceleratedCheckoutsConfiguration, ...) to an ancestor view.") } return configuration } @@ -125,6 +141,19 @@ public struct AcceleratedCheckoutButtons: View { @available(iOS 16.0, *) extension AcceleratedCheckoutButtons { + /// Controls the loading UI shown while shop settings are being fetched. + /// + /// The default `.automatic` presentation renders neutral placeholders matching the + /// number, height, spacing, and corner radius of the configured wallet buttons. + /// Use `.hidden` when the containing app provides its own loading UI. + public func loadingPresentation( + _ presentation: ShopifyAcceleratedCheckouts.LoadingPresentation + ) -> AcceleratedCheckoutButtons { + var newView = self + newView.loadingPresentation = presentation + return newView + } + public func applePayButtonType(_ type: PKPaymentButtonType) -> AcceleratedCheckoutButtons { var view = self view.applePayButtonType = type diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift index a79b188a1..b7b8c321e 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/Wallet.swift @@ -36,11 +36,23 @@ final class CheckoutProtocolClientContainer: Sendable { extension View { func walletButtonStyle(bg: Color = Color.black, cornerRadius: CGFloat? = nil) -> some View { - let defaultCornerRadius: CGFloat = 8 - let radius = cornerRadius ?? defaultCornerRadius - return frame(height: 48) + let radius = WalletButtonLayout.resolvedCornerRadius(cornerRadius) + return frame(height: WalletButtonLayout.height) .background(bg) - .clipShape(RoundedRectangle(cornerRadius: radius >= 0 ? radius : defaultCornerRadius)) + .clipShape(RoundedRectangle(cornerRadius: radius)) + } +} + +enum WalletButtonLayout { + static let height: CGFloat = 48 + static let spacing: CGFloat = 8 + static let defaultCornerRadius: CGFloat = 8 + + static func resolvedCornerRadius(_ cornerRadius: CGFloat?) -> CGFloat { + guard let cornerRadius, cornerRadius >= 0 else { + return defaultCornerRadius + } + return cornerRadius } } diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/WalletButtonSkeleton.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/WalletButtonSkeleton.swift new file mode 100644 index 000000000..147271895 --- /dev/null +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/WalletButtonSkeleton.swift @@ -0,0 +1,26 @@ +import SwiftUI + +@available(iOS 16.0, *) +struct WalletButtonSkeleton: View { + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @State private var isPulsing = false + + let cornerRadius: CGFloat? + + var body: some View { + RoundedRectangle( + cornerRadius: WalletButtonLayout.resolvedCornerRadius(cornerRadius) + ) + .fill(Color(uiColor: .tertiarySystemFill)) + .frame(height: WalletButtonLayout.height) + .opacity(reduceMotion ? 0.7 : (isPulsing ? 0.5 : 1)) + .animation( + reduceMotion ? nil : .easeInOut(duration: 0.9).repeatForever(autoreverses: true), + value: isPulsing + ) + .onAppear { + isPulsing = true + } + .accessibilityHidden(true) + } +} diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/QueryCacheTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/QueryCacheTests.swift new file mode 100644 index 000000000..6fa9f26fc --- /dev/null +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Internal/StorefrontAPI/QueryCacheTests.swift @@ -0,0 +1,145 @@ +@testable import ShopifyAcceleratedCheckouts +import XCTest + +@available(iOS 17.0, *) +final class QueryCacheTests: XCTestCase { + func testConcurrentMissesShareRequest() async throws { + let cache = QueryCache() + let counter = QueryCounter() + + async let first: Int = cache.load(cacheKey: "shop", url: queryCacheTestURL) { + try await Task.sleep(for: .milliseconds(20)) + return await counter.increment() + } + async let second: Int = cache.load(cacheKey: "shop", url: queryCacheTestURL) { + try await Task.sleep(for: .milliseconds(20)) + return await counter.increment() + } + + let values = try await [first, second] + XCTAssertEqual(values, [1, 1]) + let requestCount = await counter.value + XCTAssertEqual(requestCount, 1) + } + + func testFreshValueIsReturnedWithoutAnotherRequest() async throws { + let cache = QueryCache(freshnessInterval: 3600) + let counter = QueryCounter() + let initialDate = Date(timeIntervalSinceReferenceDate: 1000) + + let first: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate + ) { + await counter.increment() + } + let second: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate.addingTimeInterval(3599) + ) { + await counter.increment() + } + + XCTAssertEqual(first, 1) + XCTAssertEqual(second, 1) + let requestCount = await counter.value + XCTAssertEqual(requestCount, 1) + } + + func testStaleValueReturnsWhileCacheRefreshes() async throws { + let cache = QueryCache(freshnessInterval: 3600) + let counter = QueryCounter() + let initialDate = Date(timeIntervalSinceReferenceDate: 1000) + + let first: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate + ) { + await counter.increment() + } + let stale: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate.addingTimeInterval(3601) + ) { + await counter.increment() + } + + XCTAssertEqual(first, 1) + XCTAssertEqual(stale, 1) + + await waitForCount(2, counter: counter) + + let refreshed: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate.addingTimeInterval(3602) + ) { + await counter.increment() + } + XCTAssertEqual(refreshed, 2) + let requestCount = await counter.value + XCTAssertEqual(requestCount, 2) + } + + func testFailedRefreshKeepsStaleValue() async throws { + let cache = QueryCache(freshnessInterval: 1) + let counter = QueryCounter() + let initialDate = Date(timeIntervalSinceReferenceDate: 1000) + + let first: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate + ) { + await counter.increment() + } + let stale: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate.addingTimeInterval(2) + ) { + _ = await counter.increment() + throw QueryCacheTestError.refreshFailed + } + + XCTAssertEqual(first, 1) + XCTAssertEqual(stale, 1) + await waitForCount(2, counter: counter) + + let retained: Int = try await cache.load( + cacheKey: "shop", + url: queryCacheTestURL, + date: initialDate.addingTimeInterval(2) + ) { + throw QueryCacheTestError.refreshFailed + } + XCTAssertEqual(retained, 1) + } + + private func waitForCount(_ count: Int, counter: QueryCounter) async { + for _ in 0 ..< 100 { + if await counter.value >= count { return } + try? await Task.sleep(for: .milliseconds(1)) + } + XCTFail("Timed out waiting for query count \(count)") + } +} + +private let queryCacheTestURL = URL(string: "https://example.invalid/api/graphql.json")! + +private actor QueryCounter { + private(set) var value = 0 + + func increment() -> Int { + value += 1 + return value + } +} + +private enum QueryCacheTestError: Error { + case refreshFailed +} diff --git a/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/AcceleratedCheckoutButtonsLoadingTests.swift b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/AcceleratedCheckoutButtonsLoadingTests.swift new file mode 100644 index 000000000..d14d42ca5 --- /dev/null +++ b/platforms/swift/Tests/ShopifyAcceleratedCheckoutsTests/Wallets/AcceleratedCheckoutButtonsLoadingTests.swift @@ -0,0 +1,46 @@ +@testable import ShopifyAcceleratedCheckouts +import ViewInspector +import XCTest + +@available(iOS 17.0, *) +@MainActor +final class AcceleratedCheckoutButtonsLoadingTests: XCTestCase { + private let validCartID = "gid://shopify/Cart/test-cart-id" + + func testLoadingSkeletonCountMatchesWallets() throws { + let buttons = AcceleratedCheckoutButtons(cartID: validCartID) + .wallets([.shopPay, .applePay]) + + let skeletons = try buttons.inspect().findAll(WalletButtonSkeleton.self) + + XCTAssertEqual(skeletons.count, 2) + } + + func testLoadingPresentationHiddenRemovesSkeletons() throws { + let buttons = AcceleratedCheckoutButtons(cartID: validCartID) + .wallets([.shopPay, .applePay]) + .loadingPresentation(.hidden) + + let skeletons = try buttons.inspect().findAll(WalletButtonSkeleton.self) + + XCTAssertTrue(skeletons.isEmpty) + XCTAssertEqual(buttons.loadingPresentation, .hidden) + } + + func testInvalidIdentifierDoesNotRenderSkeletons() throws { + let buttons = AcceleratedCheckoutButtons(cartID: "invalid") + + let skeletons = try buttons.inspect().findAll(WalletButtonSkeleton.self) + + XCTAssertTrue(skeletons.isEmpty) + } + + func testSkeletonLayoutMatchesWalletButtons() { + XCTAssertEqual(WalletButtonLayout.height, 48) + XCTAssertEqual(WalletButtonLayout.spacing, 8) + XCTAssertEqual(WalletButtonLayout.resolvedCornerRadius(nil), 8) + XCTAssertEqual(WalletButtonLayout.resolvedCornerRadius(-1), 8) + XCTAssertEqual(WalletButtonLayout.resolvedCornerRadius(0), 0) + XCTAssertEqual(WalletButtonLayout.resolvedCornerRadius(24), 24) + } +} diff --git a/platforms/swift/api/ShopifyAcceleratedCheckouts.json b/platforms/swift/api/ShopifyAcceleratedCheckouts.json index c3cd12ff1..3caded4f5 100644 --- a/platforms/swift/api/ShopifyAcceleratedCheckouts.json +++ b/platforms/swift/api/ShopifyAcceleratedCheckouts.json @@ -880,6 +880,236 @@ } ] }, + { + "kind": "TypeDecl", + "name": "LoadingPresentation", + "printedName": "LoadingPresentation", + "children": [ + { + "kind": "Var", + "name": "automatic", + "printedName": "automatic", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation.Type) -> ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "children": [ + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO9automaticyA2DmF", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO9automaticyA2DmF", + "moduleName": "ShopifyAcceleratedCheckouts" + }, + { + "kind": "Var", + "name": "hidden", + "printedName": "hidden", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation.Type) -> ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "children": [ + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO6hiddenyA2DmF", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO6hiddenyA2DmF", + "moduleName": "ShopifyAcceleratedCheckouts" + }, + { + "kind": "Function", + "name": "__derived_enum_equals", + "printedName": "__derived_enum_equals(_:_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + }, + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + }, + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + } + ], + "declKind": "Func", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO21__derived_enum_equalsySbAD_ADtFZ", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO21__derived_enum_equalsySbAD_ADtFZ", + "moduleName": "ShopifyAcceleratedCheckouts", + "static": true, + "implicit": true, + "declAttributes": [ + "Implements" + ], + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "hash", + "printedName": "hash(into:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "Hasher", + "printedName": "Swift.Hasher", + "paramValueOwnership": "InOut", + "usr": "s:s6HasherV" + } + ], + "declKind": "Func", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO4hash4intoys6HasherVz_tF", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO4hash4intoys6HasherVz_tF", + "moduleName": "ShopifyAcceleratedCheckouts", + "implicit": true, + "funcSelfKind": "NonMutating" + }, + { + "kind": "Var", + "name": "hashValue", + "printedName": "hashValue", + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Var", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO9hashValueSivp", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO9hashValueSivp", + "moduleName": "ShopifyAcceleratedCheckouts", + "implicit": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Accessor", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO9hashValueSivg", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO9hashValueSivg", + "moduleName": "ShopifyAcceleratedCheckouts", + "implicit": true, + "accessorKind": "get" + } + ] + } + ], + "declKind": "Enum", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO", + "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO", + "moduleName": "ShopifyAcceleratedCheckouts", + "isFromExtension": true, + "isEnumExhaustive": true, + "conformances": [ + { + "kind": "Conformance", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH", + "mangledName": "$sSH" + }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ", + "mangledName": "$sSQ" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + } + ] + }, { "kind": "TypeDecl", "name": "RequiredContactFields", @@ -1939,6 +2169,35 @@ "Available" ] }, + { + "kind": "Function", + "name": "loadingPresentation", + "printedName": "loadingPresentation(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "AcceleratedCheckoutButtons", + "printedName": "ShopifyAcceleratedCheckouts.AcceleratedCheckoutButtons", + "usr": "s:27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV" + }, + { + "kind": "TypeNominal", + "name": "LoadingPresentation", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.LoadingPresentation", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO19LoadingPresentationO" + } + ], + "declKind": "Func", + "usr": "s:27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV19loadingPresentationyAc2AO07LoadingG0OF", + "mangledName": "$s27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV19loadingPresentationyAc2AO07LoadingG0OF", + "moduleName": "ShopifyAcceleratedCheckouts", + "declAttributes": [ + "Preconcurrency", + "Custom" + ], + "isFromExtension": true, + "funcSelfKind": "NonMutating" + }, { "kind": "Function", "name": "applePayButtonType", @@ -3536,6 +3795,90 @@ } ] }, + { + "kind": "TypeDecl", + "name": "View", + "printedName": "View", + "children": [ + { + "kind": "Function", + "name": "shopifyAcceleratedCheckouts", + "printedName": "shopifyAcceleratedCheckouts(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "OpaqueTypeArchetype", + "printedName": "some SwiftUI.View", + "children": [ + { + "kind": "TypeNominal", + "name": "View", + "printedName": "SwiftUI.View", + "usr": "s:7SwiftUI4ViewP" + } + ] + }, + { + "kind": "TypeNominal", + "name": "Configuration", + "printedName": "ShopifyAcceleratedCheckouts.ShopifyAcceleratedCheckouts.Configuration", + "usr": "s:27ShopifyAcceleratedCheckoutsAAO13ConfigurationV" + } + ], + "declKind": "Func", + "usr": "s:7SwiftUI4ViewP27ShopifyAcceleratedCheckoutsE07shopifyeF0yQrA2DO13ConfigurationVF", + "mangledName": "$s7SwiftUI4ViewP27ShopifyAcceleratedCheckoutsE07shopifyeF0yQrA2DO13ConfigurationVF", + "moduleName": "ShopifyAcceleratedCheckouts", + "genericSig": "", + "declAttributes": [ + "Preconcurrency", + "Custom" + ], + "isFromExtension": true, + "funcSelfKind": "NonMutating" + } + ], + "declKind": "Protocol", + "usr": "s:7SwiftUI4ViewP", + "mangledName": "$s7SwiftUI4ViewP", + "moduleName": "SwiftUICore", + "genericSig": "", + "intro_Macosx": "10.15", + "intro_iOS": "13.0", + "intro_tvOS": "13.0", + "intro_watchOS": "6.0", + "declAttributes": [ + "Preconcurrency", + "TypeEraser", + "TypeEraser", + "OriginallyDefinedIn", + "OriginallyDefinedIn", + "OriginallyDefinedIn", + "OriginallyDefinedIn", + "Available", + "Available", + "Available", + "Available", + "Custom" + ], + "isExternal": true, + "conformances": [ + { + "kind": "Conformance", + "name": "Escapable", + "printedName": "Escapable", + "usr": "s:s9EscapableP", + "mangledName": "$ss9EscapableP" + }, + { + "kind": "Conformance", + "name": "Copyable", + "printedName": "Copyable", + "usr": "s:s8CopyableP", + "mangledName": "$ss8CopyableP" + } + ] + }, { "kind": "TypeDecl", "name": "PKPaymentAuthorizationController", From 9ee3b744cb22ebb8f6eb3a34534c565770f9b08c Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Mon, 24 Aug 2026 14:45:40 +0100 Subject: [PATCH 2/3] [Swift] Log accelerated checkout loading performance --- platforms/swift/README.md | 4 ++ .../StorefrontAPI/StorefrontAPI+Queries.swift | 67 +++++++++++++++++-- ...fyAcceleratedCheckouts+Configuration.swift | 9 ++- .../ShopifyAcceleratedCheckouts.swift | 21 ++++++ .../Wallets/AcceleratedCheckoutButtons.swift | 11 +++ 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/platforms/swift/README.md b/platforms/swift/README.md index da02d2bc1..721ef8121 100644 --- a/platforms/swift/README.md +++ b/platforms/swift/README.md @@ -497,6 +497,10 @@ shop settings used by the wallet buttons. Requests are deduplicated across butto settings remain fresh for one hour and stale settings are served while they refresh in the background. Existing direct environment injection remains supported. +Set `ShopifyAcceleratedCheckouts.logLevel = .debug` to inspect privacy-safe request, cache +freshness, prefetch, and wallet button loading timings. These logs do not include shop +configuration values or checkout identifiers. + Use one customer mode at a time: ```swift diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift index a6bc38661..e99e16ce0 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Internal/StorefrontAPI/StorefrontAPI+Queries.swift @@ -27,11 +27,26 @@ extension StorefrontAPI { cacheKey: "shop", url: client.url, query: { - let response = try await client.query(Operations.getShop()) - guard let shop = response.data?.shop else { - throw StorefrontAPI.Errors.payload(propertyName: "shop") + let requestStartedAt = AcceleratedCheckoutDebugTiming.now + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings Storefront API request started." + ) + + do { + let response = try await client.query(Operations.getShop()) + guard let shop = response.data?.shop else { + throw StorefrontAPI.Errors.payload(propertyName: "shop") + } + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings Storefront API request completed in \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: requestStartedAt)) ms." + ) + return shop + } catch { + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings Storefront API request failed after \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: requestStartedAt)) ms." + ) + throw error } - return shop } ) } @@ -70,16 +85,32 @@ actor QueryCache { let key = buildCacheKey(queryKey: cacheKey, url: url) if let entry = cache[key], let cached = entry.value as? T { - if date.timeIntervalSince(entry.cachedAt) >= freshnessInterval { + let age = max(0, date.timeIntervalSince(entry.cachedAt)) + let isFresh = age < freshnessInterval + let freshnessRemaining = max(0, freshnessInterval - age) + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache hit: \(isFresh ? "fresh" : "stale") " + + "(age: \(AcceleratedCheckoutDebugTiming.seconds(age)) s, " + + "freshness: \(AcceleratedCheckoutDebugTiming.seconds(freshnessInterval)) s, " + + "remaining: \(AcceleratedCheckoutDebugTiming.seconds(freshnessRemaining)) s)." + ) + if !isFresh { refreshIfNeeded(key: key, date: date, query: query) } return cached } if let existingTask = inflightRequests[key] as? Task { + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache miss: joining an in-flight request." + ) return try await existingTask.value } + let loadStartedAt = AcceleratedCheckoutDebugTiming.now + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache miss: starting a request." + ) let task = Task { try await query() } @@ -90,9 +121,16 @@ actor QueryCache { let result = try await task.value cache(result, for: key, date: date) inflightRequests.removeValue(forKey: key) + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache filled in \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: loadStartedAt)) ms; " + + "fresh for \(AcceleratedCheckoutDebugTiming.seconds(freshnessInterval)) s." + ) return result } catch { inflightRequests.removeValue(forKey: key) + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache fill failed after \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: loadStartedAt)) ms." + ) throw error } } @@ -102,8 +140,17 @@ actor QueryCache { date: Date, query: @Sendable @escaping () async throws -> T ) { - guard inflightRequests[key] == nil else { return } + guard inflightRequests[key] == nil else { + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache refresh already in flight; serving the stale value." + ) + return + } + let refreshStartedAt = AcceleratedCheckoutDebugTiming.now + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache refresh started; serving the stale value." + ) let task = Task { try await query() } @@ -113,8 +160,14 @@ actor QueryCache { do { let result = try await task.value cache(result, for: key, date: date) + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache refresh completed in \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: refreshStartedAt)) ms; " + + "fresh for \(AcceleratedCheckoutDebugTiming.seconds(freshnessInterval)) s." + ) } catch { - // Keep serving the stale value and retry on a future load. + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings cache refresh failed after \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: refreshStartedAt)) ms; retaining the stale value." + ) } inflightRequests.removeValue(forKey: key) } diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift index 610a9e43b..24c76ada9 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts+Configuration.swift @@ -100,6 +100,8 @@ private struct ShopSettingsPrefetchIdentity: Equatable { @available(iOS 16.0, *) enum ShopSettingsPrefetcher { static func prefetch(configuration: ShopifyAcceleratedCheckouts.Configuration) async { + let startedAt = AcceleratedCheckoutDebugTiming.now + ShopifyAcceleratedCheckouts.logger.debug("Shop settings prefetch started.") let storefront = StorefrontAPI( storefrontDomain: configuration.storefrontDomain, storefrontAccessToken: configuration.storefrontAccessToken @@ -107,8 +109,13 @@ enum ShopSettingsPrefetcher { do { _ = try await storefront.shop() + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings prefetch completed in \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: startedAt)) ms." + ) } catch { - ShopifyAcceleratedCheckouts.logger.debug("Shop settings prefetch failed.") + ShopifyAcceleratedCheckouts.logger.debug( + "Shop settings prefetch failed after \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: startedAt)) ms." + ) } } } diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift index 97b0c2cb3..46cd61e48 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift @@ -1,3 +1,4 @@ +import Foundation import ShopifyCheckoutKit public enum ShopifyAcceleratedCheckouts { @@ -21,3 +22,23 @@ public enum ShopifyAcceleratedCheckouts { /// To modify the logLevel internal static let logger = OSLogger(prefix: name, logLevel: .warn) } + +@available(iOS 16.0, *) +enum AcceleratedCheckoutDebugTiming { + private static let clock = ContinuousClock() + + static var now: ContinuousClock.Instant { + clock.now + } + + static func elapsedMilliseconds(since start: ContinuousClock.Instant) -> String { + let components = start.duration(to: clock.now).components + let milliseconds = Double(components.seconds) * 1000 + + Double(components.attoseconds) / 1_000_000_000_000_000 + return String(format: "%.1f", milliseconds) + } + + static func seconds(_ interval: TimeInterval) -> String { + String(format: "%.1f", interval) + } +} diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift index 5c4bf3cfc..ea958ab17 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift @@ -119,6 +119,11 @@ public struct AcceleratedCheckoutButtons: View { private func loadShopSettings() async { guard identifier.isValid() else { return } + let startedAt = AcceleratedCheckoutDebugTiming.now + ShopifyAcceleratedCheckouts.logger.debug( + "Wallet button loading started (wallets: \(wallets.count), skeletons: \(loadingPresentation == .automatic ? wallets.count : 0))." + ) + do { currentRenderState = .loading let configuration = resolvedConfiguration @@ -129,10 +134,16 @@ public struct AcceleratedCheckoutButtons: View { let shop = try await storefront.shop() shopSettings = ShopSettings(from: shop) currentRenderState = .rendered + ShopifyAcceleratedCheckouts.logger.debug( + "Wallet buttons rendered in \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: startedAt)) ms (wallets: \(wallets.count))." + ) } catch { let reason = "Error loading shop settings: \(error)" ShopifyAcceleratedCheckouts.logger.error(reason) currentRenderState = .error(reason: reason) + ShopifyAcceleratedCheckouts.logger.debug( + "Wallet button loading failed after \(AcceleratedCheckoutDebugTiming.elapsedMilliseconds(since: startedAt)) ms." + ) } } } From 42bb3478b482671b3f511dc0f3e86b4d14f30821 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Mon, 24 Aug 2026 14:54:12 +0100 Subject: [PATCH 3/3] [Swift] Configure accelerated checkout in demo app --- .../Sources/App/AppDelegate.swift | 8 ++++ .../Sources/App/SceneDelegate.swift | 38 +++++++++++++++++-- .../Sources/Scenes/Cart/CartView.swift | 14 ------- .../Sources/Scenes/ProductView.swift | 14 ------- .../Sources/Scenes/SettingsView.swift | 23 +++++++++++ 5 files changed, 66 insertions(+), 31 deletions(-) diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/AppDelegate.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/AppDelegate.swift index 479f11352..cff96c1c5 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/AppDelegate.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/AppDelegate.swift @@ -1,3 +1,4 @@ +import ShopifyAcceleratedCheckouts import ShopifyCheckoutKit import UIKit @@ -21,6 +22,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let checkoutKitLogLevel: LogLevel = getLogLevel( key: AppStorageKeys.checkoutKitLogLevel.rawValue ) + let acceleratedCheckoutsLogLevel: LogLevel = getLogLevel( + key: AppStorageKeys.acceleratedCheckoutsLogLevel.rawValue + ) let checkoutPreloadingEnabled = UserDefaults.standard.object( forKey: AppStorageKeys.checkoutPreloadingEnabled.rawValue ) as? Bool ?? true @@ -32,8 +36,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { $0.logLevel = checkoutKitLogLevel $0.preloading.enabled = checkoutPreloadingEnabled } + ShopifyAcceleratedCheckouts.logLevel = acceleratedCheckoutsLogLevel print("[CheckoutKitSwiftDemo] CheckoutKit Log level set to \(checkoutKitLogLevel)") + print( + "[CheckoutKitSwiftDemo] Accelerated Checkouts Log level set to \(acceleratedCheckoutsLogLevel)" + ) UIBarButtonItem.appearance().tintColor = ColorPalette.primaryColor diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift index e5ffbfb19..72959a354 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/App/SceneDelegate.swift @@ -1,4 +1,5 @@ import Combine +import ShopifyAcceleratedCheckouts import ShopifyCheckoutKit import SwiftUI import UIKit @@ -17,9 +18,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var cancellables: Set = [] let uiKitCartController = CartViewController() - let swiftUICartController = UIHostingController(rootView: CartView()) - let productGridController = UIHostingController(rootView: ProductGridView()) - let productGalleryController = UIHostingController(rootView: ProductGalleryView()) + let swiftUICartController = UIHostingController( + rootView: AcceleratedCheckoutsConfiguredView(content: CartView()) + ) + let productGridController = UIHostingController( + rootView: AcceleratedCheckoutsConfiguredView(content: ProductGridView()) + ) + let productGalleryController = UIHostingController( + rootView: AcceleratedCheckoutsConfiguredView(content: ProductGalleryView()) + ) let accountController = UIHostingController(rootView: AccountView()) let settingsController = UIHostingController(rootView: SettingsView()) @@ -309,6 +316,31 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } +struct AcceleratedCheckoutsConfiguredView: View { + let content: Content + + var body: some View { + if #available(iOS 16.0, *) { + content + .shopifyAcceleratedCheckouts( + ShopifyAcceleratedCheckouts.Configuration( + storefrontDomain: InfoDictionary.shared.domain, + storefrontAccessToken: InfoDictionary.shared.accessToken + ) + ) + .environment( + \.shopifyApplePayConfiguration, + ShopifyAcceleratedCheckouts.ApplePayConfiguration( + merchantIdentifier: InfoDictionary.shared.merchantIdentifier, + contactFields: [.email, .phone] + ) + ) + } else { + content + } + } +} + extension Notification.Name { static let colorSchemeChanged = Notification.Name("colorSchemeChanged") static let navigateToAccount = Notification.Name("navigateToAccount") diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift index 46dadcfec..18deee4b9 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/Cart/CartView.swift @@ -50,20 +50,6 @@ struct CartView: View { print("[AcceleratedCheckout] Dismissed") } .connect(client) - .environment( - \.shopifyAcceleratedCheckoutsConfiguration, - ShopifyAcceleratedCheckouts.Configuration( - storefrontDomain: InfoDictionary.shared.domain, - storefrontAccessToken: InfoDictionary.shared.accessToken - ) - ) - .environment( - \.shopifyApplePayConfiguration, - ShopifyAcceleratedCheckouts.ApplePayConfiguration( - merchantIdentifier: InfoDictionary.shared.merchantIdentifier, - contactFields: [.email, .phone] - ) - ) } } diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/ProductView.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/ProductView.swift index 57d828c3e..f00ee8cbf 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/ProductView.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/ProductView.swift @@ -123,20 +123,6 @@ struct ProductView: View { .onDismiss { print("[AcceleratedCheckout] Dismissed") } - .environment( - \.shopifyAcceleratedCheckoutsConfiguration, - ShopifyAcceleratedCheckouts.Configuration( - storefrontDomain: InfoDictionary.shared.domain, - storefrontAccessToken: InfoDictionary.shared.accessToken - ) - ) - .environment( - \.shopifyApplePayConfiguration, - ShopifyAcceleratedCheckouts.ApplePayConfiguration( - merchantIdentifier: InfoDictionary.shared.merchantIdentifier, - contactFields: [.email, .phone] - ) - ) } } }.padding([.leading, .trailing], 15) diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/SettingsView.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/SettingsView.swift index 573202944..836e4df0e 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/SettingsView.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Scenes/SettingsView.swift @@ -1,5 +1,6 @@ import Combine import PassKit +import ShopifyAcceleratedCheckouts import ShopifyCheckoutKit import SwiftUI @@ -36,6 +37,13 @@ struct SettingsView: View { } } + @AppStorage(AppStorageKeys.acceleratedCheckoutsLogLevel.rawValue) + var acceleratedCheckoutsLogLevel: LogLevel = .debug { + didSet { + ShopifyAcceleratedCheckouts.logLevel = acceleratedCheckoutsLogLevel + } + } + @AppStorage(AppStorageKeys.applePayStyle.rawValue) var applePayStyle: ApplePayStyleOption = .automatic @@ -160,6 +168,21 @@ struct SettingsView: View { } .pickerStyle(.menu) + Picker( + "Accelerated Checkouts", + selection: Binding( + get: { acceleratedCheckoutsLogLevel }, + set: { acceleratedCheckoutsLogLevel = $0 } + ) + ) { + ForEach(LogLevel.allCases, id: \.self) { level in + Text( + level.rawValue.capitalized(with: Locale.current) + ).tag(level) + } + } + .pickerStyle(.menu) + NavigationLink(destination: LogsView()) { Text("Logs") }