diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift index b1a93c69..b1c5564e 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutBridge.swift @@ -28,6 +28,7 @@ enum BridgeError: Swift.Error { case unencodableInstrumentation(Swift.Error? = nil) } +@MainActor protocol CheckoutBridgeProtocol { static func instrument(_ webView: WKWebView, _ instrumentation: InstrumentationPayload) static func sendMessage(_ webView: WKWebView, messageName: String, messageBody: String?) @@ -95,25 +96,23 @@ enum CheckoutBridge: CheckoutBridgeProtocol { } static func sendResponse(_ webView: WKWebView, messageBody: String) { - DispatchQueue.main.async { - let script = """ - (function() { - try { - if (window.EmbeddedCheckoutProtocol && typeof window.EmbeddedCheckoutProtocol.postMessage === 'function') { - window.EmbeddedCheckoutProtocol.postMessage(\(messageBody)); - } else if (window && window.console && window.console.error) { - window.console.error('EmbeddedCheckoutProtocol.postMessage is not available.'); - } - } catch (error) { - if (window && window.console && window.console.error) { - window.console.error('Failed to post message to checkout', error); - } + let script = """ + (function() { + try { + if (window.EmbeddedCheckoutProtocol && typeof window.EmbeddedCheckoutProtocol.postMessage === 'function') { + window.EmbeddedCheckoutProtocol.postMessage(\(messageBody)); + } else if (window && window.console && window.console.error) { + window.console.error('EmbeddedCheckoutProtocol.postMessage is not available.'); } - })(); - """ + } catch (error) { + if (window && window.console && window.console.error) { + window.console.error('Failed to post message to checkout', error); + } + } + })(); + """ - webView.evaluateJavaScript(script) - } + webView.evaluateJavaScript(script) } static func dispatchMessageTemplate(body: String) -> String { diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift index e5600c5b..4957b013 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift @@ -27,6 +27,7 @@ import SwiftUI import UIKit +@MainActor public class CheckoutViewController: UINavigationController { public init(checkout url: URL, client: (any CheckoutCommunicationProtocol)? = nil) { let rootViewController = CheckoutWebViewController(checkoutURL: url, client: client, entryPoint: nil) @@ -108,34 +109,39 @@ public struct CheckoutSheet: UIViewControllerRepresentable, CheckoutConfigurable } public protocol CheckoutConfigurable { - func backgroundColor(_ color: UIColor) -> Self - func colorScheme(_ colorScheme: ShopifyCheckoutKit.Configuration.ColorScheme) -> Self - func tintColor(_ color: UIColor) -> Self - func title(_ title: String) -> Self - func closeButtonTintColor(_ color: UIColor?) -> Self + @MainActor func backgroundColor(_ color: UIColor) -> Self + @MainActor func colorScheme(_ colorScheme: ShopifyCheckoutKit.Configuration.ColorScheme) -> Self + @MainActor func tintColor(_ color: UIColor) -> Self + @MainActor func title(_ title: String) -> Self + @MainActor func closeButtonTintColor(_ color: UIColor?) -> Self } extension CheckoutConfigurable { + @MainActor @discardableResult public func backgroundColor(_ color: UIColor) -> Self { ShopifyCheckoutKit.configuration.backgroundColor = color return self } + @MainActor @discardableResult public func colorScheme(_ colorScheme: ShopifyCheckoutKit.Configuration.ColorScheme) -> Self { ShopifyCheckoutKit.configuration.colorScheme = colorScheme return self } + @MainActor @discardableResult public func tintColor(_ color: UIColor) -> Self { ShopifyCheckoutKit.configuration.tintColor = color return self } + @MainActor @discardableResult public func title(_ title: String) -> Self { ShopifyCheckoutKit.configuration.title = title return self } + @MainActor @discardableResult public func closeButtonTintColor(_ color: UIColor?) -> Self { ShopifyCheckoutKit.configuration.closeButtonTintColor = color return self diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift index 672073e2..8666f2dc 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebView.swift @@ -27,6 +27,7 @@ import UIKit import WebKit +@MainActor protocol CheckoutWebViewDelegate: AnyObject { func checkoutViewDidStartNavigation() func checkoutViewDidFinishNavigation() @@ -34,6 +35,7 @@ protocol CheckoutWebViewDelegate: AnyObject { func checkoutViewDidFailWithError(error: CheckoutError) } +@MainActor class CheckoutWebView: WKWebView { private static var cache: CacheEntry? var timer: Date? @@ -166,10 +168,12 @@ class CheckoutWebView: WKWebView { deinit { OSLogger.shared.debug("De-allocating webview") - if isRecovery { - navigationObserver?.invalidate() - } else { - detachBridge() + MainActor.assumeIsolated { + if isRecovery { + navigationObserver?.invalidate() + } else { + detachBridge() + } } } @@ -243,7 +247,7 @@ class CheckoutWebView: WKWebView { } } -extension CheckoutWebView: WKScriptMessageHandler { +extension CheckoutWebView: @preconcurrency WKScriptMessageHandler { func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { guard let body = message.body as? String else { return @@ -258,7 +262,7 @@ extension CheckoutWebView: WKScriptMessageHandler { return } - Task { + Task { @MainActor in if let response = await client.process(body) { checkoutBridge.sendResponse(self, messageBody: response) } @@ -266,7 +270,7 @@ extension CheckoutWebView: WKScriptMessageHandler { } } -extension CheckoutWebView: WKNavigationDelegate { +extension CheckoutWebView: @preconcurrency WKNavigationDelegate { func webView(_: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = action.request.url else { decisionHandler(.allow) diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift index a76f11d3..24b95365 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift @@ -24,6 +24,7 @@ import UIKit import WebKit +@MainActor class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControllerDelegate { var onCancel: (() -> Void)? var onFail: ((CheckoutError) -> Void)? diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/ConfettiCannon.swift b/platforms/swift/Sources/ShopifyCheckoutKit/ConfettiCannon.swift index df985e28..0743330e 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/ConfettiCannon.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/ConfettiCannon.swift @@ -23,6 +23,7 @@ import UIKit +@MainActor enum ConfettiCannon { static func fire(in view: UIView) { let layerName = "shopify-confetti" diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/ProgressBarView.swift b/platforms/swift/Sources/ShopifyCheckoutKit/ProgressBarView.swift index 0182903c..7292bb47 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/ProgressBarView.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/ProgressBarView.swift @@ -23,6 +23,7 @@ import UIKit +@MainActor class ProgressBarView: UIView { lazy var progressBar: UIProgressView = { let progressBar = UIProgressView(progressViewStyle: .bar) diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift b/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift index b6da73a9..3645a7a9 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift @@ -29,9 +29,10 @@ import UIKit /// The version of the `ShopifyCheckoutKit` library. public let version = "3.8.0" -var invalidateOnConfigurationChange = true +@MainActor var invalidateOnConfigurationChange = true /// The configuration options for the `ShopifyCheckoutKit` library. +@MainActor public var configuration = Configuration() { didSet { if invalidateOnConfigurationChange { @@ -42,11 +43,13 @@ public var configuration = Configuration() { } /// A convienence function for configuring the `ShopifyCheckoutKit` library. +@MainActor public func configure(_ block: (inout Configuration) -> Void) { block(&configuration) } /// Preloads the checkout for faster presentation. +@MainActor public func preload(checkout url: URL) { guard configuration.preloading.enabled else { return @@ -58,10 +61,12 @@ public func preload(checkout url: URL) { } /// Invalidate the checkout cache from preload calls +@MainActor public func invalidate() { CheckoutWebView.invalidate(disconnect: true) } +@MainActor @discardableResult public func present(checkout url: URL, from: UIViewController, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController { let decorated = CheckoutProtocol.url(for: url, colorScheme: configuration.colorScheme.rawValue) @@ -70,6 +75,7 @@ public func present(checkout url: URL, from: UIViewController, client: (any Chec return viewController } +@MainActor @discardableResult package func present(checkout url: URL, from: UIViewController, entryPoint: MetaData.EntryPoint, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController { let decorated = CheckoutProtocol.url(for: url, colorScheme: configuration.colorScheme.rawValue)