diff --git a/Package.swift b/Package.swift index 11a7d44e..0cdc17aa 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,7 @@ let package = Package( ), .target( name: "ShopifyAcceleratedCheckouts", - dependencies: ["ShopifyCheckoutKit"], + dependencies: ["ShopifyCheckoutKit", "ShopifyCheckoutProtocol"], path: "platforms/swift/Sources/ShopifyAcceleratedCheckouts", resources: [.process("Localizable.xcstrings"), .process("Media.xcassets")] ), diff --git a/platforms/android/lib/api/lib.api b/platforms/android/lib/api/lib.api index 87e74d00..2af0f3d2 100644 --- a/platforms/android/lib/api/lib.api +++ b/platforms/android/lib/api/lib.api @@ -305,10 +305,6 @@ public final class com/shopify/checkoutkit/Checkout$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public abstract interface class com/shopify/checkoutkit/CheckoutCommunicationClient { - public abstract fun process (Ljava/lang/String;)Ljava/lang/String; -} - public final class com/shopify/checkoutkit/CheckoutDiscounts { public static final field Companion Lcom/shopify/checkoutkit/CheckoutDiscounts$Companion; public fun ()V @@ -427,7 +423,7 @@ public abstract interface class com/shopify/checkoutkit/CheckoutListener { } public final class com/shopify/checkoutkit/CheckoutPresentation { - public final fun connect (Lcom/shopify/checkoutkit/CheckoutCommunicationClient;)V + public final fun connect (Lcom/shopify/checkoutkit/CheckoutProtocol$Client;)V public final fun onCancel (Lkotlin/jvm/functions/Function0;)V public final fun onFail (Lkotlin/jvm/functions/Function1;)V public final fun onGeolocationPermissionsHidePrompt (Lkotlin/jvm/functions/Function0;)V @@ -448,11 +444,10 @@ public final class com/shopify/checkoutkit/CheckoutProtocol { public final fun getWindowOpen ()Lcom/shopify/checkoutkit/DelegationDescriptor; } -public final class com/shopify/checkoutkit/CheckoutProtocol$Client : com/shopify/checkoutkit/CheckoutCommunicationClient { +public final class com/shopify/checkoutkit/CheckoutProtocol$Client { public fun ()V public final fun on (Lcom/shopify/checkoutkit/DelegationDescriptor;Lkotlin/jvm/functions/Function1;)Lcom/shopify/checkoutkit/CheckoutProtocol$Client; public final fun on (Lcom/shopify/checkoutkit/NotificationDescriptor;Lkotlin/jvm/functions/Function1;)Lcom/shopify/checkoutkit/CheckoutProtocol$Client; - public fun process (Ljava/lang/String;)Ljava/lang/String; } public final class com/shopify/checkoutkit/CheckoutStatus : java/lang/Enum { @@ -2502,9 +2497,9 @@ public final class com/shopify/checkoutkit/ShopifyCheckoutKit { public static final fun invalidate ()V public static final fun preload (Ljava/lang/String;Landroidx/activity/ComponentActivity;)V public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutListener;)Lcom/shopify/checkoutkit/CheckoutKitDialog; - public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutListener;Lcom/shopify/checkoutkit/CheckoutCommunicationClient;)Lcom/shopify/checkoutkit/CheckoutKitDialog; + public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutListener;Lcom/shopify/checkoutkit/CheckoutProtocol$Client;)Lcom/shopify/checkoutkit/CheckoutKitDialog; public static final synthetic fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lkotlin/jvm/functions/Function1;)Lcom/shopify/checkoutkit/CheckoutKitDialog; - public static synthetic fun present$default (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutListener;Lcom/shopify/checkoutkit/CheckoutCommunicationClient;ILjava/lang/Object;)Lcom/shopify/checkoutkit/CheckoutKitDialog; + public static synthetic fun present$default (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutListener;Lcom/shopify/checkoutkit/CheckoutProtocol$Client;ILjava/lang/Object;)Lcom/shopify/checkoutkit/CheckoutKitDialog; } public final class com/shopify/checkoutkit/Signals { diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutCommunicationClient.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutCommunicationClient.kt index aba87754..b34e4f02 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutCommunicationClient.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutCommunicationClient.kt @@ -1,12 +1,10 @@ package com.shopify.checkoutkit /** - * Implement this interface to handle Embedded Checkout Protocol (ECP) messages beyond - * the built-in methods handled natively by the SDK. - * - * Register an implementation via [ShopifyCheckoutKit.present]. + * Internal bridge abstraction for processing raw Embedded Checkout Protocol (ECP) + * messages after the WebView has filtered them to supported methods. */ -public interface CheckoutCommunicationClient { +internal interface CheckoutCommunicationClient { /** * Process a JSON-RPC 2.0 ECP message from the checkout web page. * @@ -18,5 +16,14 @@ public interface CheckoutCommunicationClient { * @param message JSON-RPC 2.0 encoded message string * @return JSON-RPC 2.0 encoded response string, or null to send no response */ - public fun process(message: String): String? + fun process(message: String): String? +} + +internal fun CheckoutProtocol.Client.asCommunicationClient(): CheckoutCommunicationClient = + CheckoutProtocolClientAdapter(this) + +private class CheckoutProtocolClientAdapter( + private val client: CheckoutProtocol.Client, +) : CheckoutCommunicationClient { + override fun process(message: String): String? = client.process(message) } diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt index 85eb9355..f71883c1 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutDialog.kt @@ -28,7 +28,7 @@ internal class CheckoutDialog( private val checkoutUrl: String, private val checkoutListener: CheckoutListener, context: Context, - private val communicationClient: CheckoutCommunicationClient? = null, + private val communicationClient: CheckoutProtocol.Client? = null, ) : ComponentDialog(context) { private var presentedCheckoutWebView: CheckoutWebView? = null diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutPresentation.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutPresentation.kt index f434ba1c..a5bda056 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutPresentation.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutPresentation.kt @@ -21,7 +21,7 @@ public class CheckoutPresentation internal constructor() { internal var onGeolocationPermissionsShowPrompt: ((String, GeolocationPermissions.Callback) -> Unit)? = null internal var onGeolocationPermissionsHidePrompt: (() -> Unit)? = null - internal var communicationClient: CheckoutCommunicationClient? = null + internal var communicationClient: CheckoutProtocol.Client? = null /** * Called when checkout fails. @@ -76,7 +76,7 @@ public class CheckoutPresentation internal constructor() { /** * Connects a communication client for Embedded Checkout Protocol messages. */ - public fun connect(client: CheckoutCommunicationClient?) { + public fun connect(client: CheckoutProtocol.Client?) { communicationClient = client } diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutProtocol.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutProtocol.kt index 95c22bfa..afcb1587 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutProtocol.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutProtocol.kt @@ -23,7 +23,7 @@ import java.util.concurrent.CountDownLatch * Entry point for the typed Embedded Checkout Protocol (ECP) client. * * Provides static [NotificationDescriptor] instances for every EC notification method, - * plus a fluent [Client] builder that implements [CheckoutCommunicationClient]. + * plus a fluent [Client] builder for typed protocol callbacks. * * Example usage: * ```kotlin @@ -140,7 +140,7 @@ public object CheckoutProtocol { internal val json: Json = Json { ignoreUnknownKeys = true } /** - * A typed, fluent implementation of [CheckoutCommunicationClient]. + * A typed, fluent protocol client. * * Each [on] call returns a new [Client] instance (value semantics), * making it safe to share a base configuration across multiple presents. @@ -148,7 +148,7 @@ public object CheckoutProtocol { public class Client private constructor( private val handlers: Map, private val delegations: Map, - ) : CheckoutCommunicationClient { + ) { public constructor() : this(emptyMap(), emptyMap()) @@ -185,7 +185,7 @@ public object CheckoutProtocol { ): Client = Client(handlers, delegations + (descriptor.method to Delegation.Typed(descriptor, handler))) /** Called by [EmbeddedCheckoutProtocol] for every delegated EC message. */ - override fun process(message: String): String? = + internal fun process(message: String): String? = decodeRequest(message)?.let { request -> val delegation = delegations[request.method] if (delegation != null) { diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt index f353c4ad..f3505263 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt @@ -38,9 +38,9 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n this.listener = listener } - fun setClient(client: CheckoutCommunicationClient?) { + fun setClient(client: CheckoutProtocol.Client?) { log.d(LOG_TAG, "Setting communication client $client.") - embeddedCheckoutProtocol.setClient(client) + embeddedCheckoutProtocol.setClient(client?.asCommunicationClient()) } fun markPresented() { diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocol.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocol.kt index 5920335f..5cc7d36e 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocol.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocol.kt @@ -26,13 +26,14 @@ internal class EmbeddedCheckoutProtocol( ) { private val decoder = Json { ignoreUnknownKeys = true } private val defaultClient: CheckoutProtocol.Client = defaultDelegationClient() + private val defaultCommunicationClient: CheckoutCommunicationClient = defaultClient.asCommunicationClient() private val defaultClientBindings: Map = mapOf( CheckoutProtocol.windowOpen.method to DefaultClientBinding( - client = defaultClient, + client = defaultCommunicationClient, policy = DefaultClientPolicy.RunIfUnhandled, ), CheckoutProtocol.error.method to DefaultClientBinding( - client = defaultClient, + client = defaultCommunicationClient, policy = DefaultClientPolicy.AlwaysRunAfterMerchant, ), CheckoutProtocol.complete.method to DefaultClientBinding( diff --git a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/ShopifyCheckoutKit.kt b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/ShopifyCheckoutKit.kt index c82fc5b1..18fc1e4a 100644 --- a/platforms/android/lib/src/main/java/com/shopify/checkoutkit/ShopifyCheckoutKit.kt +++ b/platforms/android/lib/src/main/java/com/shopify/checkoutkit/ShopifyCheckoutKit.kt @@ -117,7 +117,7 @@ public object ShopifyCheckoutKit { * @param context The context the checkout is being presented from * @param checkoutListener provides callbacks to allow clients to listen for and respond to checkout lifecycle events * (failure, cancellation, permission prompts, file chooser). - * @param communicationClient optional handler for supported Embedded Checkout Protocol (ECP) + * @param communicationClient optional typed handler for supported Embedded Checkout Protocol (ECP) * messages from the checkout web page. Built-in messages * (`ec.ready` and [ec.start][CheckoutProtocol.start]) * are handled automatically by the SDK. @@ -129,7 +129,7 @@ public object ShopifyCheckoutKit { checkoutUrl: String, context: ComponentActivity, checkoutListener: T, - communicationClient: CheckoutCommunicationClient? = null, + communicationClient: CheckoutProtocol.Client? = null, ): CheckoutKitDialog? { log.d("ShopifyCheckoutKit", "Present called with checkoutUrl ${checkoutUrl.redactedUrlForLogging()}.") if (context.isDestroyed || context.isFinishing) { diff --git a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutPresentationTest.kt b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutPresentationTest.kt index 29a4f3d7..9378a98d 100644 --- a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutPresentationTest.kt +++ b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/CheckoutPresentationTest.kt @@ -16,8 +16,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf @@ -79,10 +77,10 @@ class CheckoutPresentationTest { } @Test - fun `present builder forwards connected client to embedded checkout protocol`() { - val rawMessage = """{"jsonrpc":"2.0","method":"ec.messages.change","params":{"checkout":{}}}""" - val client = mock() - whenever(client.process(rawMessage)).thenReturn(null) + fun `present builder dispatches connected protocol client`() { + var received: Checkout? = null + val client = CheckoutProtocol.Client() + .on(CheckoutProtocol.start) { checkout -> received = checkout } ShopifyCheckoutKit.present("https://shopify.com", activity) { connect(client) @@ -92,10 +90,10 @@ class CheckoutPresentationTest { val dialog = ShadowDialog.getLatestDialog() as CheckoutDialog val webView = dialog.currentWebView() - webView.embeddedCheckoutProtocol().postMessage(rawMessage) + webView.embeddedCheckoutProtocol().postMessage(ecStartMessage(currency = "USD")) shadowOf(Looper.getMainLooper()).runToEndOfTasks() - verify(client).process(rawMessage) + assertThat(received?.currency).isEqualTo("USD") } @Test @@ -209,4 +207,19 @@ class CheckoutPresentationTest { field.isAccessible = true return field.get(this) as EmbeddedCheckoutProtocol } + + private fun ecStartMessage(currency: String): String { + val checkout = """ + { + "id":"chk1", + "currency":"$currency", + "status":"incomplete", + "line_items":[], + "totals":[], + "links":[], + "ucp":{"payment_handlers":{},"version":"1.0"} + } + """.trimIndent() + return """{"jsonrpc":"2.0","method":"ec.start","params":{"checkout":$checkout}}""" + } } diff --git a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocolTest.kt b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocolTest.kt index ed840281..2d28351f 100644 --- a/platforms/android/lib/src/test/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocolTest.kt +++ b/platforms/android/lib/src/test/java/com/shopify/checkoutkit/EmbeddedCheckoutProtocolTest.kt @@ -258,7 +258,7 @@ class EmbeddedCheckoutProtocolTest { fun `window open falls back to kit default when consumer client has no handler`() { registerFakeBrowserFor("https://example.com") // Empty typed client — no .on(CheckoutProtocol.windowOpen) registered. - ecp.setClient(CheckoutProtocol.Client()) + ecp.setClient(CheckoutProtocol.Client().asCommunicationClient()) val js = captureEvaluatedJs { ecp.postMessage(windowOpenRequest(id = "\"8\"", url = "https://example.com")) @@ -276,7 +276,7 @@ class EmbeddedCheckoutProtocolTest { .on(CheckoutProtocol.windowOpen) { _ -> WindowOpenResult.Rejected(reason = "merchant says no") } - ecp.setClient(merchantClient) + ecp.setClient(merchantClient.asCommunicationClient()) val js = captureEvaluatedJs { ecp.postMessage(windowOpenRequest(id = "\"8\"", url = "https://example.com")) @@ -297,7 +297,7 @@ class EmbeddedCheckoutProtocolTest { captured = request WindowOpenResult.Success } - ecp.setClient(merchantClient) + ecp.setClient(merchantClient.asCommunicationClient()) ecp.postMessage(windowOpenRequest(id = "\"8\"", url = "https://example.com/promo?id=42")) shadowOf(Looper.getMainLooper()).runToEndOfTasks() @@ -525,7 +525,7 @@ class EmbeddedCheckoutProtocolTest { val rawMessage = """{"jsonrpc":"2.0","method":"ec.error","params":{"error":{$ERROR_RESPONSE_UCP}}}""" val client = CheckoutProtocol.Client() .on(CheckoutProtocol.error) { fail("Malformed ec.error should not dispatch") } - ecp.setClient(client) + ecp.setClient(client.asCommunicationClient()) ecp.postMessage(rawMessage) shadowOf(Looper.getMainLooper()).runToEndOfTasks() diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/test/java/com/shopify/reactnative/checkoutkit/ProtocolRelayTest.kt b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/test/java/com/shopify/reactnative/checkoutkit/ProtocolRelayTest.kt index 4ff53a95..f1067f96 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/test/java/com/shopify/reactnative/checkoutkit/ProtocolRelayTest.kt +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/test/java/com/shopify/reactnative/checkoutkit/ProtocolRelayTest.kt @@ -2,6 +2,7 @@ package com.shopify.reactnative.checkoutkit import android.os.Looper import android.util.Log +import com.shopify.checkoutkit.CheckoutProtocol import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString @@ -44,7 +45,7 @@ class ProtocolRelayTest { DispatchCallback { json -> captured = json }, ) - client.process(ecStartNotificationFixture) + client.processForTest(ecStartNotificationFixture) shadowOf(Looper.getMainLooper()).runToEndOfTasks() val json = captured @@ -73,7 +74,7 @@ class ProtocolRelayTest { DispatchCallback { throw failure }, ) - client.process(ecStartNotificationFixture) + client.processForTest(ecStartNotificationFixture) shadowOf(Looper.getMainLooper()).runToEndOfTasks() val logs = ShadowLog.getLogsForTag("ShopifyCheckoutKit") @@ -100,7 +101,7 @@ class ProtocolRelayTest { DispatchCallback { json -> captured = json }, ) - client.process(checkoutNotificationFixture(method)) + client.processForTest(checkoutNotificationFixture(method)) shadowOf(Looper.getMainLooper()).runToEndOfTasks() val json = captured @@ -119,7 +120,7 @@ class ProtocolRelayTest { DispatchCallback { json -> captured = json }, ) - client.process(ecErrorNotificationFixture) + client.processForTest(ecErrorNotificationFixture) shadowOf(Looper.getMainLooper()).runToEndOfTasks() val json = captured @@ -141,7 +142,7 @@ class ProtocolRelayTest { DispatchCallback { json -> captured = json }, ) - client.process(ecStartNotificationFixture) + client.processForTest(ecStartNotificationFixture) shadowOf(Looper.getMainLooper()).runToEndOfTasks() assertThat(captured).isNull() @@ -157,7 +158,7 @@ class ProtocolRelayTest { ) dispatch.release() - client.process(ecStartNotificationFixture) + client.processForTest(ecStartNotificationFixture) shadowOf(Looper.getMainLooper()).runToEndOfTasks() assertThat(captured).isNull() @@ -170,6 +171,15 @@ private data class SnakePayload( @SerialName("line_items") val lineItems: List, ) +private fun CheckoutProtocol.Client.processForTest(message: String): String? { + val processMethod = javaClass.declaredMethods.first { + it.name.startsWith("process") && + it.parameterTypes.contentEquals(arrayOf(String::class.java)) + } + processMethod.isAccessible = true + return processMethod.invoke(this, message) as? String +} + private fun checkoutNotificationFixture(method: String) = ecStartNotificationFixture.replace( "\"method\": \"ec.start\"", "\"method\": \"$method\"", diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ProtocolRelay.swift b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ProtocolRelay.swift index da135ca2..5801d28a 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ProtocolRelay.swift +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ProtocolRelay.swift @@ -1,8 +1,6 @@ import Foundation #if COCOAPODS import ShopifyCheckoutKit - - extension CheckoutProtocol.Client: @retroactive CheckoutCommunicationProtocol {} #else import ShopifyCheckoutProtocol #endif diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift index 2a7910db..737aa77f 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/Wallets/AcceleratedCheckoutButtons.swift @@ -1,5 +1,8 @@ import PassKit import ShopifyCheckoutKit +#if !COCOAPODS + import ShopifyCheckoutProtocol +#endif import SwiftUI /// Render state for AcceleratedCheckoutButtons @@ -232,7 +235,7 @@ extension AcceleratedCheckoutButtons { return newView } - public func connect(_ client: (any CheckoutCommunicationProtocol)?) -> AcceleratedCheckoutButtons { + public func connect(_ client: CheckoutProtocol.Client?) -> AcceleratedCheckoutButtons { var newView = self newView.clientContainer = CheckoutProtocolClientContainer(client) return newView diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutCommunicationProtocol.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutCommunicationProtocol.swift index 709d9889..6a677c93 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutCommunicationProtocol.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutCommunicationProtocol.swift @@ -3,7 +3,7 @@ #endif import Foundation -public protocol CheckoutCommunicationProtocol: Sendable { +package protocol CheckoutCommunicationProtocol: Sendable { func process(_ message: String) async -> String? } diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift index 0f59fc30..3b190f15 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutViewController.swift @@ -6,7 +6,7 @@ import UIKit @MainActor public class CheckoutViewController: UINavigationController { - public init(checkout url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) { + public init(checkout url: URL, delegate: (any CheckoutDelegate)? = nil, client: CheckoutProtocol.Client? = nil) { let rootViewController = CheckoutWebViewController(checkoutURL: url, delegate: delegate, client: client, entryPoint: nil) super.init(rootViewController: rootViewController) presentationController?.delegate = rootViewController @@ -62,7 +62,13 @@ public struct ShopifyCheckout: UIViewControllerRepresentable, CheckoutConfigurab webViewController.onFail = onFailAction } - @discardableResult public func connect(_ handler: any CheckoutCommunicationProtocol) -> Self { + @discardableResult public func connect(_ handler: CheckoutProtocol.Client) -> Self { + var copy = self + copy.client = handler + return copy + } + + @discardableResult package func connect(_ handler: any CheckoutCommunicationProtocol) -> Self { var copy = self copy.client = handler return copy diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift index c26347c3..70af8128 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/CheckoutWebViewController.swift @@ -55,7 +55,7 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl // MARK: Initializers - public init(checkoutURL url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) { + init(checkoutURL url: URL, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil, entryPoint: MetaData.EntryPoint? = nil) { checkoutURL = url self.delegate = delegate self.client = client diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift b/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift index 32f56790..e44772ab 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift @@ -60,7 +60,16 @@ public func invalidate() { @MainActor @discardableResult -public func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil, client: (any CheckoutCommunicationProtocol)? = nil) -> CheckoutViewController { +public func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil, client: CheckoutProtocol.Client? = nil) -> CheckoutViewController { + let decorated = CheckoutProtocol.url(for: url) + let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client) + from.present(viewController, animated: true) + return viewController +} + +@MainActor +@discardableResult +package func present(checkout url: URL, from: UIViewController, delegate: (any CheckoutDelegate)? = nil, client: any CheckoutCommunicationProtocol) -> CheckoutViewController { let decorated = CheckoutProtocol.url(for: url) let viewController = CheckoutViewController(checkout: decorated, delegate: delegate, client: client) from.present(viewController, animated: true) diff --git a/platforms/swift/api/ShopifyAcceleratedCheckouts.json b/platforms/swift/api/ShopifyAcceleratedCheckouts.json index 0fa362c9..cd65439a 100644 --- a/platforms/swift/api/ShopifyAcceleratedCheckouts.json +++ b/platforms/swift/api/ShopifyAcceleratedCheckouts.json @@ -60,6 +60,13 @@ "declKind": "Import", "moduleName": "ShopifyAcceleratedCheckouts" }, + { + "kind": "Import", + "name": "ShopifyCheckoutProtocol", + "printedName": "ShopifyCheckoutProtocol", + "declKind": "Import", + "moduleName": "ShopifyAcceleratedCheckouts" + }, { "kind": "Import", "name": "SwiftOnoneSupport", @@ -2220,21 +2227,21 @@ { "kind": "TypeNominal", "name": "Optional", - "printedName": "(any ShopifyCheckoutKit.CheckoutCommunicationProtocol)?", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client?", "children": [ { "kind": "TypeNominal", - "name": "CheckoutCommunicationProtocol", - "printedName": "any ShopifyCheckoutKit.CheckoutCommunicationProtocol", - "usr": "s:18ShopifyCheckoutKit0B21CommunicationProtocolP" + "name": "Client", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client", + "usr": "s:23ShopifyCheckoutProtocol0bC0O6ClientV" } ], "usr": "s:Sq" } ], "declKind": "Func", - "usr": "s:27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV7connectyAC0aD3Kit0D21CommunicationProtocol_pSgF", - "mangledName": "$s27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV7connectyAC0aD3Kit0D21CommunicationProtocol_pSgF", + "usr": "s:27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV7connectyAC0aD8Protocol0dG0O6ClientVSgF", + "mangledName": "$s27ShopifyAcceleratedCheckouts0B15CheckoutButtonsV7connectyAC0aD8Protocol0dG0O6ClientVSgF", "moduleName": "ShopifyAcceleratedCheckouts", "declAttributes": [ "Preconcurrency", diff --git a/platforms/swift/api/ShopifyCheckoutKit.json b/platforms/swift/api/ShopifyCheckoutKit.json index 217349a5..1d96c27a 100644 --- a/platforms/swift/api/ShopifyCheckoutKit.json +++ b/platforms/swift/api/ShopifyCheckoutKit.json @@ -153,6 +153,7 @@ "moduleName": "ShopifyCheckoutKit", "genericSig": "", "protocolReq": true, + "isInternal": true, "reqNewWitnessTableEntry": true, "funcSelfKind": "NonMutating" } @@ -162,6 +163,7 @@ "mangledName": "$s18ShopifyCheckoutKit0B21CommunicationProtocolP", "moduleName": "ShopifyCheckoutKit", "genericSig": "", + "isInternal": true, "conformances": [ { "kind": "Conformance", @@ -1229,13 +1231,13 @@ { "kind": "TypeNominal", "name": "Optional", - "printedName": "(any ShopifyCheckoutKit.CheckoutCommunicationProtocol)?", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client?", "children": [ { "kind": "TypeNominal", - "name": "CheckoutCommunicationProtocol", - "printedName": "any ShopifyCheckoutKit.CheckoutCommunicationProtocol", - "usr": "s:18ShopifyCheckoutKit0B21CommunicationProtocolP" + "name": "Client", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client", + "usr": "s:23ShopifyCheckoutProtocol0bC0O6ClientV" } ], "hasDefaultArg": true, @@ -1243,8 +1245,8 @@ } ], "declKind": "Constructor", - "usr": "s:18ShopifyCheckoutKit0B14ViewControllerC8checkout8delegate6clientAC10Foundation3URLV_AA0B8Delegate_pSgAA0B21CommunicationProtocol_pSgtcfc", - "mangledName": "$s18ShopifyCheckoutKit0B14ViewControllerC8checkout8delegate6clientAC10Foundation3URLV_AA0B8Delegate_pSgAA0B21CommunicationProtocol_pSgtcfc", + "usr": "s:18ShopifyCheckoutKit0B14ViewControllerC8checkout8delegate6clientAC10Foundation3URLV_AA0B8Delegate_pSg0aB8Protocol0bL0O6ClientVSgtcfc", + "mangledName": "$s18ShopifyCheckoutKit0B14ViewControllerC8checkout8delegate6clientAC10Foundation3URLV_AA0B8Delegate_pSg0aB8Protocol0bL0O6ClientVSgtcfc", "moduleName": "ShopifyCheckoutKit", "declAttributes": [ "Custom" @@ -1812,6 +1814,35 @@ ], "funcSelfKind": "NonMutating" }, + { + "kind": "Function", + "name": "connect", + "printedName": "connect(_:)", + "children": [ + { + "kind": "TypeNominal", + "name": "ShopifyCheckout", + "printedName": "ShopifyCheckoutKit.ShopifyCheckout", + "usr": "s:18ShopifyCheckoutKit0aB0V" + }, + { + "kind": "TypeNominal", + "name": "Client", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client", + "usr": "s:23ShopifyCheckoutProtocol0bC0O6ClientV" + } + ], + "declKind": "Func", + "usr": "s:18ShopifyCheckoutKit0aB0V7connectyAC0aB8Protocol0bE0O6ClientVF", + "mangledName": "$s18ShopifyCheckoutKit0aB0V7connectyAC0aB8Protocol0bE0O6ClientVF", + "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Preconcurrency", + "DiscardableResult", + "Custom" + ], + "funcSelfKind": "NonMutating" + }, { "kind": "Function", "name": "connect", @@ -1834,6 +1865,7 @@ "usr": "s:18ShopifyCheckoutKit0aB0V7connectyAcA0B21CommunicationProtocol_pF", "mangledName": "$s18ShopifyCheckoutKit0aB0V7connectyAcA0B21CommunicationProtocol_pF", "moduleName": "ShopifyCheckoutKit", + "isInternal": true, "declAttributes": [ "Preconcurrency", "DiscardableResult", @@ -5888,13 +5920,13 @@ { "kind": "TypeNominal", "name": "Optional", - "printedName": "(any ShopifyCheckoutKit.CheckoutCommunicationProtocol)?", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client?", "children": [ { "kind": "TypeNominal", - "name": "CheckoutCommunicationProtocol", - "printedName": "any ShopifyCheckoutKit.CheckoutCommunicationProtocol", - "usr": "s:18ShopifyCheckoutKit0B21CommunicationProtocolP" + "name": "Client", + "printedName": "ShopifyCheckoutProtocol.CheckoutProtocol.Client", + "usr": "s:23ShopifyCheckoutProtocol0bC0O6ClientV" } ], "hasDefaultArg": true, @@ -5902,8 +5934,8 @@ } ], "declKind": "Func", - "usr": "s:18ShopifyCheckoutKit7present8checkout4from8delegate6clientAA0B14ViewControllerC10Foundation3URLV_So06UIViewJ0CAA0B8Delegate_pSgAA0B21CommunicationProtocol_pSgtF", - "mangledName": "$s18ShopifyCheckoutKit7present8checkout4from8delegate6clientAA0B14ViewControllerC10Foundation3URLV_So06UIViewJ0CAA0B8Delegate_pSgAA0B21CommunicationProtocol_pSgtF", + "usr": "s:18ShopifyCheckoutKit7present8checkout4from8delegate6clientAA0B14ViewControllerC10Foundation3URLV_So06UIViewJ0CAA0B8Delegate_pSg0aB8Protocol0bO0O6ClientVSgtF", + "mangledName": "$s18ShopifyCheckoutKit7present8checkout4from8delegate6clientAA0B14ViewControllerC10Foundation3URLV_So06UIViewJ0CAA0B8Delegate_pSg0aB8Protocol0bO0O6ClientVSgtF", "moduleName": "ShopifyCheckoutKit", "declAttributes": [ "DiscardableResult", @@ -5911,6 +5943,62 @@ ], "funcSelfKind": "NonMutating" }, + { + "kind": "Function", + "name": "present", + "printedName": "present(checkout:from:delegate:client:)", + "children": [ + { + "kind": "TypeNominal", + "name": "CheckoutViewController", + "printedName": "ShopifyCheckoutKit.CheckoutViewController", + "usr": "c:@M@ShopifyCheckoutKit@objc(cs)CheckoutViewController" + }, + { + "kind": "TypeNominal", + "name": "URL", + "printedName": "Foundation.URL", + "usr": "s:10Foundation3URLV" + }, + { + "kind": "TypeNominal", + "name": "UIViewController", + "printedName": "UIKit.UIViewController", + "usr": "c:objc(cs)UIViewController" + }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "(any ShopifyCheckoutKit.CheckoutDelegate)?", + "children": [ + { + "kind": "TypeNominal", + "name": "CheckoutDelegate", + "printedName": "any ShopifyCheckoutKit.CheckoutDelegate", + "usr": "s:18ShopifyCheckoutKit0B8DelegateP" + } + ], + "hasDefaultArg": true, + "usr": "s:Sq" + }, + { + "kind": "TypeNominal", + "name": "CheckoutCommunicationProtocol", + "printedName": "any ShopifyCheckoutKit.CheckoutCommunicationProtocol", + "usr": "s:18ShopifyCheckoutKit0B21CommunicationProtocolP" + } + ], + "declKind": "Func", + "usr": "s:18ShopifyCheckoutKit7present8checkout4from8delegate6clientAA0B14ViewControllerC10Foundation3URLV_So06UIViewJ0CAA0B8Delegate_pSgAA0B21CommunicationProtocol_ptF", + "mangledName": "$s18ShopifyCheckoutKit7present8checkout4from8delegate6clientAA0B14ViewControllerC10Foundation3URLV_So06UIViewJ0CAA0B8Delegate_pSgAA0B21CommunicationProtocol_ptF", + "moduleName": "ShopifyCheckoutKit", + "isInternal": true, + "declAttributes": [ + "DiscardableResult", + "Custom" + ], + "funcSelfKind": "NonMutating" + }, { "kind": "Function", "name": "present",