diff --git a/platforms/swift/README.md b/platforms/swift/README.md index 081a6ab24..b485dc512 100644 --- a/platforms/swift/README.md +++ b/platforms/swift/README.md @@ -204,6 +204,21 @@ A successful background preload normally transitions from `.loading` to `.ready` | `.expired` | The cached preload exceeded its lifetime before it could be used. | | `.failed(reason:)` | An HTTP, navigation, or web-content failure occurred while preloading. | +By default, a 429 response with a valid `Retry-After` header produces a `.throttled` failure. +Checkout Kit suppresses further preload requests until the server-provided delay elapses; it does +not retry automatically. Presentation is never suppressed. + +To manage preload backoff in your application instead, use the `.passthrough` throttle policy: + +```swift +ShopifyCheckoutKit.configure { + $0.preloading.throttlePolicy = .passthrough +} +``` + +Under `.passthrough`, the failure is `.httpError(statusCode: 429, retryAfter:)` and Checkout Kit +does not suppress subsequent preload requests. + `preload` returns `nil` when preloading is disabled. Checkout Kit can reuse a matching preloaded checkout when `present` is called later: @@ -259,6 +274,7 @@ ShopifyCheckoutKit.configure { | `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. | +| `preloading.throttlePolicy` | `.managed` | Enforces server-provided preload backoff. Use `.passthrough` to receive the HTTP failure without suppressing preload requests. | | `allowedMessageOrigins` | `[]` | Origins trusted to send incoming checkout messages. Empty trusts every origin (open by default). See [Incoming message origin validation](#incoming-message-origin-validation). | To localize the title, add `shopify_checkout_kit_title` to your app's `Localizable.xcstrings`. @@ -354,9 +370,10 @@ Kit-owned link delegations such as `window.open` are offered to your connected p ### Error handling A checkout lifecycle failure is delivered as a `CheckoutError` to `checkoutDidFail(error:)` -or `.onFail`. It has a stable `code`, diagnostic `message`, optional `httpStatusCode`, and an -optional native `underlyingError`. Use the stable code for recovery and analytics. Use diagnostic -text and underlying errors only for debugging and logging. +or `.onFail`. It has a stable `code`, diagnostic `message`, optional `httpStatusCode`, optional +server-provided `retryAfter` delay in seconds, and an optional native `underlyingError`. Use the +stable code for recovery and analytics. Use diagnostic text and underlying errors only for +debugging and logging. | `CheckoutErrorCode` | Meaning | Suggested app action | | --- | --- | --- | @@ -365,7 +382,7 @@ text and underlying errors only for debugging and logging. | `.cartExpired` | The cart or checkout session is no longer available. | Create a new cart and retry. | | `.cartCompleted` | The cart has already completed checkout. | Clear or create a new cart. | | `.invalidCart` | The cart cannot continue checkout. | Create a new cart and retry. | -| `.httpError` | Checkout returned an HTTP error response. `httpStatusCode` is available. | Inspect `httpStatusCode`; retry only when it makes sense for your app. | +| `.httpError` | Checkout returned an HTTP error response. `httpStatusCode` and, when supplied by the server, `retryAfter` are available. | Inspect `httpStatusCode`; do not retry before `retryAfter`, and retry only when it makes sense for your app. | | `.networkError` | Checkout navigation failed before an HTTP response was available. | Offer a retry when connectivity is available. | | `.webContentProcessTerminated` | WebKit terminated the content process. | Let the buyer explicitly retry; Checkout Kit does not reload automatically. | | `.sdkError` | An internal Checkout Kit error has occurred (e.g. a protocol message could not be decoded). | Log diagnostic context and offer a browser fallback. | diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutError.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutError.swift index c131fc0a4..7f504d541 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutError.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutError.swift @@ -40,8 +40,9 @@ public enum CheckoutErrorCode: String, Codable, CaseIterable, Sendable { /// or ``ShopifyCheckout/onFail(_:)``. /// /// Use ``code`` for application behavior. Use ``message`` and ``underlyingError`` only for debugging -/// and logging. ``httpStatusCode`` is present only when an HTTP response caused failure. Your app owns -/// recovery actions such as retrying, recreating a cart, authenticating a buyer, and reopening checkout. +/// and logging. ``httpStatusCode`` is present only when an HTTP response caused failure. ``retryAfter`` +/// contains the server-provided delay when one is available. Your app owns recovery actions such as +/// retrying, recreating a cart, authenticating a buyer, and reopening checkout. public struct CheckoutError: LocalizedError { /// Stable code for this failure. public let code: CheckoutErrorCode @@ -52,6 +53,9 @@ public struct CheckoutError: LocalizedError { /// HTTP status for an HTTP-response failure, otherwise `nil`. public let httpStatusCode: Int? + /// Server-provided delay, in seconds, before another request should be attempted. + public let retryAfter: TimeInterval? + /// Native diagnostic cause, when one is available. This value is not guaranteed to be Sendable. public let underlyingError: (any Error)? @@ -60,11 +64,13 @@ public struct CheckoutError: LocalizedError { code: CheckoutErrorCode, message: String, httpStatusCode: Int? = nil, + retryAfter: TimeInterval? = nil, underlyingError: (any Error)? = nil ) { self.code = code self.message = message self.httpStatusCode = httpStatusCode + self.retryAfter = retryAfter self.underlyingError = underlyingError } @@ -79,11 +85,12 @@ public struct CheckoutError: LocalizedError { } extension CheckoutError { - internal static func http(statusCode: Int, message: String) -> CheckoutError { + internal static func http(statusCode: Int, message: String, retryAfter: TimeInterval? = nil) -> CheckoutError { CheckoutError( code: statusCode == 410 ? .cartExpired : .httpError, message: message, - httpStatusCode: statusCode + httpStatusCode: statusCode, + retryAfter: retryAfter ) } diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift index d537654f7..b428134dd 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift @@ -13,6 +13,8 @@ struct PreloadKey: Hashable { @MainActor final class PreloadCache { + static let throttledMessage = "Preload throttled until the server-provided Retry-After delay elapses." + private struct Entry { let key: PreloadKey let view: CheckoutWebView @@ -40,6 +42,7 @@ final class PreloadCache { private var entry: Entry? private var keepAliveTimer: Timer? private var expiryTimer: Timer? + private var throttleDeadline: Date? private(set) var state: PreloadState = .idle @@ -79,6 +82,26 @@ final class PreloadCache { observer?.receive(newState) } + func beginThrottle(for delay: TimeInterval) { + throttleDeadline = Date().addingTimeInterval(delay) + evict(with: .failed(reason: .throttled, message: Self.throttledMessage)) + } + + func isThrottleActive(at date: Date = Date()) -> Bool { + guard let throttleDeadline else { + return false + } + guard date < throttleDeadline else { + self.throttleDeadline = nil + return false + } + return true + } + + func clearThrottle() { + throttleDeadline = nil + } + /// Evicts the cached view, then notifies observers of the resulting `state`. /// Clearing before notifying ensures a preload started re-entrantly from the /// callback is not wiped by this invalidation. @@ -406,6 +429,16 @@ class CheckoutWebView: WKWebView { return } + if ShopifyCheckoutKit.configuration.preloading.throttlePolicy == .managed, + preloadCache.isThrottleActive() + { + preloadCache.transition(to: .failed( + reason: .throttled, + message: PreloadCache.throttledMessage + )) + return + } + let key = PreloadKey(url: url, entryPoint: entryPoint) guard !preloadCache.hasEntry(for: key) else { OSLogger.shared.debug("Preload cache already has matching entry") @@ -827,6 +860,7 @@ extension CheckoutWebView: WKNavigationDelegate { func handleResponse(_ response: HTTPURLResponse) -> WKNavigationResponsePolicy { let statusCode = response.statusCode + let retryAfter = RetryAfter.seconds(from: response) let errorMessageForStatusCode = HTTPURLResponse.localizedString( forStatusCode: statusCode ) @@ -836,15 +870,28 @@ extension CheckoutWebView: WKNavigationDelegate { } if statusCode >= 400 { - handleCachedViewFailure( - .httpError(statusCode: statusCode), - message: "HTTP response returned status code \(statusCode)." - ) + let message = "HTTP response returned status code \(statusCode)." + if isPreloadBackgrounded, + statusCode == 429, + let retryAfter, + ShopifyCheckoutKit.configuration.preloading.throttlePolicy == .managed + { + CheckoutWebView.preloadCache.beginThrottle(for: retryAfter) + } else { + handleCachedViewFailure( + .httpError(statusCode: statusCode, retryAfter: retryAfter), + message: message + ) + } OSLogger.shared.debug("Handling response for URL: \(LogSafeURL.string(response.url)), status code: \(statusCode)") viewDelegate?.checkoutViewDidFailWithError( - error: CheckoutError.http(statusCode: statusCode, message: errorMessageForStatusCode) + error: CheckoutError.http( + statusCode: statusCode, + message: errorMessageForStatusCode, + retryAfter: retryAfter + ) ) return .cancel diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift b/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift index 594b16796..19c1d93df 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift @@ -92,6 +92,18 @@ extension Configuration { extension Configuration { public struct Preloading: Sendable { + /// Controls how Checkout Kit handles preload throttling responses. + public enum ThrottlePolicy: Equatable, Sendable { + /// Respect the server's `Retry-After` value and suppress preload requests until it elapses. + case managed + + /// Surface the HTTP failure without suppressing subsequent preload requests. + case passthrough + } + public var enabled: Bool = true + + /// The policy used when a preload receives a throttling response. + public var throttlePolicy: ThrottlePolicy = .managed } } diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/PreloadState.swift b/platforms/swift/Sources/ShopifyCheckoutKit/PreloadState.swift index 48f1a438e..b7c118e47 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/PreloadState.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/PreloadState.swift @@ -26,8 +26,15 @@ public enum PreloadState: Equatable { /// Reason a preload cache entry was not available. public enum FailureReason: Equatable { + /// The preload was throttled and Checkout Kit is suppressing further preload requests + /// until the server-provided `Retry-After` delay elapses. + case throttled + /// The preload received an HTTP response that prevented it from loading. - case httpError(statusCode: Int) + /// + /// Under the `.passthrough` throttle policy, `retryAfter` is the server-provided delay, + /// in seconds, when a throttling response includes a valid `Retry-After` header. + case httpError(statusCode: Int, retryAfter: TimeInterval? = nil) /// Preload navigation failed. case navigationFailed diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/RetryAfter.swift b/platforms/swift/Sources/ShopifyCheckoutKit/RetryAfter.swift new file mode 100644 index 000000000..96632c16d --- /dev/null +++ b/platforms/swift/Sources/ShopifyCheckoutKit/RetryAfter.swift @@ -0,0 +1,38 @@ +import Foundation + +enum RetryAfter { + private static let dateFormats = [ + "EEE, dd MMM yyyy HH:mm:ss zzz", + "EEEE, dd-MMM-yy HH:mm:ss zzz", + "EEE MMM d HH:mm:ss yyyy" + ] + + static func seconds(from response: HTTPURLResponse, now: Date = Date()) -> TimeInterval? { + seconds(from: response.value(forHTTPHeaderField: "Retry-After"), now: now) + } + + static func seconds(from value: String?, now: Date = Date()) -> TimeInterval? { + guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { + return nil + } + + if let delay = TimeInterval(value), delay >= 0 { + return delay + } + + for format in dateFormats { + let formatter = DateFormatter() + formatter.calendar = Calendar(identifier: .gregorian) + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = format + formatter.isLenient = false + + if let date = formatter.date(from: value) { + return ceil(max(0, date.timeIntervalSince(now))) + } + } + + return nil + } +} diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift index b56d44f98..2b88fe0f6 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift @@ -242,6 +242,31 @@ class CheckoutWebViewTests: XCTestCase { } } + func test429ResponseIncludesRetryAfterOnPresentationError() throws { + try view.load(checkout: XCTUnwrap(URL(string: "https://shopify1.shopify.com/checkouts/cn/123"))) + let link = try XCTUnwrap(view.url) + let didFailWithErrorExpectation = expectation(description: "checkoutViewDidFailWithError was called") + mockDelegate.didFailWithErrorExpectation = didFailWithErrorExpectation + view.viewDelegate = mockDelegate + + let response = try XCTUnwrap( + HTTPURLResponse( + url: link, + statusCode: 429, + httpVersion: nil, + headerFields: ["Retry-After": "120"] + ) + ) + + XCTAssertEqual(view.handleResponse(response), .cancel) + wait(for: [didFailWithErrorExpectation], timeout: 3) + + let error = try XCTUnwrap(mockDelegate.errorReceived) + XCTAssertEqual(error.code, .httpError) + XCTAssertEqual(error.httpStatusCode, 429) + XCTAssertEqual(error.retryAfter, 120) + } + func testNormalresponseOnNonCheckoutURLCodeDelegation() throws { let link = try XCTUnwrap(URL(string: "https://shopify.com/resource_url")) let didFailWithErrorExpectation = expectation(description: "checkoutViewDidFailWithError was not called") diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift index 19debd3cc..0156f98db 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift @@ -42,6 +42,10 @@ class ConfigurationTests: XCTestCase { XCTAssertTrue(ShopifyCheckoutKit.configuration.preloading.enabled) } + func testPreloadingThrottlePolicyDefaultsToManaged() { + XCTAssertEqual(ShopifyCheckoutKit.configuration.preloading.throttlePolicy, .managed) + } + func testAppearanceDefaultsToStorefront() { XCTAssertEqual(ShopifyCheckoutKit.configuration.appearance, .storefront) } diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/PreloadObservabilityTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/PreloadObservabilityTests.swift index 6f800bb67..908abf556 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/PreloadObservabilityTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/PreloadObservabilityTests.swift @@ -11,12 +11,16 @@ class PreloadObservabilityTests: XCTestCase { override func setUp() async throws { try await super.setUp() ShopifyCheckoutKit.configuration.preloading.enabled = true + ShopifyCheckoutKit.configuration.preloading.throttlePolicy = .managed + CheckoutWebView.preloadCache.clearThrottle() CheckoutWebView.invalidate() } override func tearDown() async throws { CheckoutWebView.invalidate() + CheckoutWebView.preloadCache.clearThrottle() ShopifyCheckoutKit.configuration.preloading.enabled = true + ShopifyCheckoutKit.configuration.preloading.throttlePolicy = .managed try await super.tearDown() } @@ -141,25 +145,78 @@ class PreloadObservabilityTests: XCTestCase { } } - func testHTTPErrorTransitionsToFailed() throws { + func testManagedThrottleTransitionsToThrottledAndSuppressesAnotherPreload() throws { let preload = ShopifyCheckoutKit.preload(checkout: url) let view = CheckoutWebView(entryPoint: nil) _ = CheckoutWebView.preloadCache.store(view, for: PreloadKey(url: url, entryPoint: nil)) view.load(checkout: url) let link = view.url ?? url - let response = try XCTUnwrap(HTTPURLResponse(url: link, statusCode: 500, httpVersion: nil, headerFields: nil)) + let response = try XCTUnwrap( + HTTPURLResponse( + url: link, + statusCode: 429, + httpVersion: nil, + headerFields: ["Retry-After": "120"] + ) + ) _ = view.handleResponse(response) withExtendedLifetime(preload) { XCTAssertEqual( preload?.state, .failed( - reason: .httpError(statusCode: 500), - message: "HTTP response returned status code 500." + reason: .throttled, + message: PreloadCache.throttledMessage ) ) } + + let suppressed = ShopifyCheckoutKit.preload(checkout: url) + XCTAssertEqual( + suppressed?.state, + .failed( + reason: .throttled, + message: PreloadCache.throttledMessage + ) + ) + XCTAssertFalse(CheckoutWebView.preloadCache.hasEntry()) + + XCTAssertFalse(CheckoutWebView.preloadCache.isThrottleActive(at: .distantFuture)) + let resumed = ShopifyCheckoutKit.preload(checkout: url) + XCTAssertEqual(resumed?.state, .loading) + XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry()) + } + + func testPassthroughThrottleReturnsHTTPMetadataWithoutSuppressingAnotherPreload() throws { + ShopifyCheckoutKit.configuration.preloading.throttlePolicy = .passthrough + let preload = ShopifyCheckoutKit.preload(checkout: url) + let view = CheckoutWebView(entryPoint: nil) + _ = CheckoutWebView.preloadCache.store(view, for: PreloadKey(url: url, entryPoint: nil)) + view.load(checkout: url) + let link = view.url ?? url + + let response = try XCTUnwrap( + HTTPURLResponse( + url: link, + statusCode: 429, + httpVersion: nil, + headerFields: ["Retry-After": "120"] + ) + ) + _ = view.handleResponse(response) + + XCTAssertEqual( + preload?.state, + .failed( + reason: .httpError(statusCode: 429, retryAfter: 120), + message: "HTTP response returned status code 429." + ) + ) + + let replacement = ShopifyCheckoutKit.preload(checkout: url) + XCTAssertEqual(replacement?.state, .loading) + XCTAssertTrue(CheckoutWebView.preloadCache.hasEntry()) } func testNavigationFailureTransitionsToFailed() { diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/RetryAfterTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/RetryAfterTests.swift new file mode 100644 index 000000000..c51254039 --- /dev/null +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/RetryAfterTests.swift @@ -0,0 +1,31 @@ +import Foundation +@testable import ShopifyCheckoutKit +import XCTest + +final class RetryAfterTests: XCTestCase { + private let now = Date(timeIntervalSince1970: 784_111_897) + + func testParsesDelaySeconds() { + XCTAssertEqual(RetryAfter.seconds(from: " 120 ", now: now), 120) + } + + func testParsesHTTPDate() { + XCTAssertEqual( + RetryAfter.seconds(from: "Sun, 06 Nov 1994 08:51:47 GMT", now: now), + 10 + ) + } + + func testPastHTTPDateReturnsZero() { + XCTAssertEqual( + RetryAfter.seconds(from: "Sun, 06 Nov 1994 08:51:27 GMT", now: now), + 0 + ) + } + + func testMissingOrInvalidValueReturnsNil() { + XCTAssertNil(RetryAfter.seconds(from: nil, now: now)) + XCTAssertNil(RetryAfter.seconds(from: "invalid", now: now)) + XCTAssertNil(RetryAfter.seconds(from: "-1", now: now)) + } +} diff --git a/platforms/swift/api/ShopifyCheckoutKit.json b/platforms/swift/api/ShopifyCheckoutKit.json index a3c1fd074..89c696f51 100644 --- a/platforms/swift/api/ShopifyCheckoutKit.json +++ b/platforms/swift/api/ShopifyCheckoutKit.json @@ -1115,6 +1115,82 @@ } ] }, + { + "kind": "Var", + "name": "retryAfter", + "printedName": "retryAfter", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Foundation.TimeInterval?", + "children": [ + { + "kind": "TypeNameAlias", + "name": "TimeInterval", + "printedName": "Foundation.TimeInterval", + "children": [ + { + "kind": "TypeNominal", + "name": "Double", + "printedName": "Swift.Double", + "usr": "s:Sd" + } + ] + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Var", + "usr": "s:18ShopifyCheckoutKit0B5ErrorV10retryAfterSdSgvp", + "mangledName": "$s18ShopifyCheckoutKit0B5ErrorV10retryAfterSdSgvp", + "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Foundation.TimeInterval?", + "children": [ + { + "kind": "TypeNameAlias", + "name": "TimeInterval", + "printedName": "Foundation.TimeInterval", + "children": [ + { + "kind": "TypeNominal", + "name": "Double", + "printedName": "Swift.Double", + "usr": "s:Sd" + } + ] + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Accessor", + "usr": "s:18ShopifyCheckoutKit0B5ErrorV10retryAfterSdSgvg", + "mangledName": "$s18ShopifyCheckoutKit0B5ErrorV10retryAfterSdSgvg", + "moduleName": "ShopifyCheckoutKit", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "get" + } + ] + }, { "kind": "Var", "name": "underlyingError", @@ -1180,7 +1256,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(code:message:httpStatusCode:underlyingError:)", + "printedName": "init(code:message:httpStatusCode:retryAfter:underlyingError:)", "children": [ { "kind": "TypeNominal", @@ -1215,6 +1291,28 @@ "hasDefaultArg": true, "usr": "s:Sq" }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Foundation.TimeInterval?", + "children": [ + { + "kind": "TypeNameAlias", + "name": "TimeInterval", + "printedName": "Foundation.TimeInterval", + "children": [ + { + "kind": "TypeNominal", + "name": "Double", + "printedName": "Swift.Double", + "usr": "s:Sd" + } + ] + } + ], + "hasDefaultArg": true, + "usr": "s:Sq" + }, { "kind": "TypeNominal", "name": "Optional", @@ -1232,8 +1330,8 @@ } ], "declKind": "Constructor", - "usr": "s:18ShopifyCheckoutKit0B5ErrorV4code7message14httpStatusCode010underlyingD0AcA0bdI0O_SSSiSgs0D0_pSgtcfc", - "mangledName": "$s18ShopifyCheckoutKit0B5ErrorV4code7message14httpStatusCode010underlyingD0AcA0bdI0O_SSSiSgs0D0_pSgtcfc", + "usr": "s:18ShopifyCheckoutKit0B5ErrorV4code7message14httpStatusCode10retryAfter010underlyingD0AcA0bdI0O_SSSiSgSdSgs0D0_pSgtcfc", + "mangledName": "$s18ShopifyCheckoutKit0B5ErrorV4code7message14httpStatusCode10retryAfter010underlyingD0AcA0bdI0O_SSSiSgSdSgs0D0_pSgtcfc", "moduleName": "ShopifyCheckoutKit", "init_kind": "Designated" }, @@ -5418,6 +5516,235 @@ "name": "Preloading", "printedName": "Preloading", "children": [ + { + "kind": "TypeDecl", + "name": "ThrottlePolicy", + "printedName": "ThrottlePolicy", + "children": [ + { + "kind": "Var", + "name": "managed", + "printedName": "managed", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy.Type) -> ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "children": [ + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO7managedyA2GmF", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO7managedyA2GmF", + "moduleName": "ShopifyCheckoutKit" + }, + { + "kind": "Var", + "name": "passthrough", + "printedName": "passthrough", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy.Type) -> ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "children": [ + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO11passthroughyA2GmF", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO11passthroughyA2GmF", + "moduleName": "ShopifyCheckoutKit" + }, + { + "kind": "Function", + "name": "__derived_enum_equals", + "printedName": "__derived_enum_equals(_:_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + }, + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + }, + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + } + ], + "declKind": "Func", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO21__derived_enum_equalsySbAG_AGtFZ", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO21__derived_enum_equalsySbAG_AGtFZ", + "moduleName": "ShopifyCheckoutKit", + "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:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO4hash4intoys6HasherVz_tF", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO4hash4intoys6HasherVz_tF", + "moduleName": "ShopifyCheckoutKit", + "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:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO9hashValueSivp", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO9hashValueSivp", + "moduleName": "ShopifyCheckoutKit", + "implicit": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Accessor", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO9hashValueSivg", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO9hashValueSivg", + "moduleName": "ShopifyCheckoutKit", + "implicit": true, + "accessorKind": "get" + } + ] + } + ], + "declKind": "Enum", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO", + "moduleName": "ShopifyCheckoutKit", + "isEnumExhaustive": true, + "conformances": [ + { + "kind": "Conformance", + "name": "Hashable", + "printedName": "Hashable", + "usr": "s:SH", + "mangledName": "$sSH" + }, + { + "kind": "Conformance", + "name": "Equatable", + "printedName": "Equatable", + "usr": "s:SQ", + "mangledName": "$sSQ" + }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "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": "Var", "name": "enabled", @@ -5490,6 +5817,79 @@ "accessorKind": "set" } ] + }, + { + "kind": "Var", + "name": "throttlePolicy", + "printedName": "throttlePolicy", + "children": [ + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + } + ], + "declKind": "Var", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14throttlePolicyAE08ThrottleG0Ovp", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14throttlePolicyAE08ThrottleG0Ovp", + "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "HasInitialValue", + "HasStorage" + ], + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + } + ], + "declKind": "Accessor", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14throttlePolicyAE08ThrottleG0Ovg", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14throttlePolicyAE08ThrottleG0Ovg", + "moduleName": "ShopifyCheckoutKit", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + }, + { + "kind": "TypeNominal", + "name": "ThrottlePolicy", + "printedName": "ShopifyCheckoutKit.Configuration.Preloading.ThrottlePolicy", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14ThrottlePolicyO" + } + ], + "declKind": "Accessor", + "usr": "s:18ShopifyCheckoutKit13ConfigurationV10PreloadingV14throttlePolicyAE08ThrottleG0Ovs", + "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV10PreloadingV14throttlePolicyAE08ThrottleG0Ovs", + "moduleName": "ShopifyCheckoutKit", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "set" + } + ] } ], "declKind": "Struct", @@ -7128,6 +7528,43 @@ "name": "FailureReason", "printedName": "FailureReason", "children": [ + { + "kind": "Var", + "name": "throttled", + "printedName": "throttled", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyCheckoutKit.PreloadState.FailureReason.Type) -> ShopifyCheckoutKit.PreloadState.FailureReason", + "children": [ + { + "kind": "TypeNominal", + "name": "FailureReason", + "printedName": "ShopifyCheckoutKit.PreloadState.FailureReason", + "usr": "s:18ShopifyCheckoutKit12PreloadStateO13FailureReasonO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyCheckoutKit.PreloadState.FailureReason.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "FailureReason", + "printedName": "ShopifyCheckoutKit.PreloadState.FailureReason", + "usr": "s:18ShopifyCheckoutKit12PreloadStateO13FailureReasonO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:18ShopifyCheckoutKit12PreloadStateO13FailureReasonO9throttledyA2EmF", + "mangledName": "$s18ShopifyCheckoutKit12PreloadStateO13FailureReasonO9throttledyA2EmF", + "moduleName": "ShopifyCheckoutKit" + }, { "kind": "Var", "name": "httpError", @@ -7136,12 +7573,12 @@ { "kind": "TypeFunc", "name": "Function", - "printedName": "(ShopifyCheckoutKit.PreloadState.FailureReason.Type) -> (Swift.Int) -> ShopifyCheckoutKit.PreloadState.FailureReason", + "printedName": "(ShopifyCheckoutKit.PreloadState.FailureReason.Type) -> (Swift.Int, Foundation.TimeInterval?) -> ShopifyCheckoutKit.PreloadState.FailureReason", "children": [ { "kind": "TypeFunc", "name": "Function", - "printedName": "(Swift.Int) -> ShopifyCheckoutKit.PreloadState.FailureReason", + "printedName": "(Swift.Int, Foundation.TimeInterval?) -> ShopifyCheckoutKit.PreloadState.FailureReason", "children": [ { "kind": "TypeNominal", @@ -7152,13 +7589,34 @@ { "kind": "TypeNominal", "name": "Tuple", - "printedName": "(statusCode: Swift.Int)", + "printedName": "(statusCode: Swift.Int, retryAfter: Foundation.TimeInterval?)", "children": [ { "kind": "TypeNominal", "name": "Int", "printedName": "Swift.Int", "usr": "s:Si" + }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "Foundation.TimeInterval?", + "children": [ + { + "kind": "TypeNameAlias", + "name": "TimeInterval", + "printedName": "Foundation.TimeInterval", + "children": [ + { + "kind": "TypeNominal", + "name": "Double", + "printedName": "Swift.Double", + "usr": "s:Sd" + } + ] + } + ], + "usr": "s:Sq" } ] } @@ -7181,8 +7639,8 @@ } ], "declKind": "EnumElement", - "usr": "s:18ShopifyCheckoutKit12PreloadStateO13FailureReasonO9httpErroryAESi_tcAEmF", - "mangledName": "$s18ShopifyCheckoutKit12PreloadStateO13FailureReasonO9httpErroryAESi_tcAEmF", + "usr": "s:18ShopifyCheckoutKit12PreloadStateO13FailureReasonO9httpErroryAESi_SdSgtcAEmF", + "mangledName": "$s18ShopifyCheckoutKit12PreloadStateO13FailureReasonO9httpErroryAESi_SdSgtcAEmF", "moduleName": "ShopifyCheckoutKit" }, {