11package com.shopify.checkoutkit
22
3- import android.content.Context
43import android.webkit.JavascriptInterface
54import com.shopify.checkoutkit.ShopifyCheckoutKit.log
65import kotlinx.serialization.Serializable
@@ -29,7 +28,7 @@ internal class EmbeddedCheckoutProtocol(
2928 @Volatile private var client : CheckoutCommunicationClient ? = null ,
3029) {
3130 private val decoder = Json { ignoreUnknownKeys = true }
32- private val defaultClient: CheckoutProtocol .Client = defaultDelegationClient(view.context )
31+ private val defaultClient: CheckoutProtocol .Client = defaultDelegationClient()
3332
3433 internal fun setClient (client : CheckoutCommunicationClient ? ) {
3534 this .client = client
@@ -48,9 +47,9 @@ internal class EmbeddedCheckoutProtocol(
4847 // ep.cart.* is out of scope for the checkout bridge
4948 request.method.startsWith(" ep." ) ->
5049 log.d(LOG_TAG , " Ignoring out-of-scope ep method: ${request.method} ." )
51- request.method == METHOD_WINDOW_OPEN_REQUEST -> handleWindowOpenRequest(message)
52- request.method == METHOD_START -> handleStart(message)
53- request.method == METHOD_COMPLETE -> handleComplete(message)
50+ request.method == CheckoutProtocol .windowOpen.method -> handleWindowOpenRequest(message)
51+ request.method == CheckoutProtocol .start.method -> handleStart(message)
52+ request.method == CheckoutProtocol .complete.method -> handleComplete(message)
5453 else -> handleClientMessage(request.method, message)
5554 }
5655 } catch (e: SerializationException ) {
@@ -101,15 +100,15 @@ internal class EmbeddedCheckoutProtocol(
101100 )
102101
103102 private fun handleStart (message : String ) {
104- log.d(LOG_TAG , " Handling $METHOD_START : hiding progress bar and bubbling up." )
103+ log.d(LOG_TAG , " Handling ${ CheckoutProtocol .start.method} : hiding progress bar and bubbling up." )
105104 onMainThread {
106105 view.getListener().onCheckoutViewLoadComplete()
107106 client?.process(message)
108107 }
109108 }
110109
111110 private fun handleComplete (message : String ) {
112- log.d(LOG_TAG , " Handling $METHOD_COMPLETE : bubbling up." )
111+ log.d(LOG_TAG , " Handling ${ CheckoutProtocol .complete.method} : bubbling up." )
113112 onMainThread {
114113 client?.process(message)
115114 }
@@ -124,7 +123,7 @@ internal class EmbeddedCheckoutProtocol(
124123 * `Intent.ACTION_VIEW` (see [defaultDelegationClient]).
125124 */
126125 private fun handleWindowOpenRequest (message : String ) {
127- log.d(LOG_TAG , " Handling $METHOD_WINDOW_OPEN_REQUEST " )
126+ log.d(LOG_TAG , " Handling ${ CheckoutProtocol .windowOpen.method} " )
128127 onMainThread {
129128 val merchantResponse = client?.process(message)
130129 if (merchantResponse != null ) {
@@ -135,12 +134,22 @@ internal class EmbeddedCheckoutProtocol(
135134 }
136135 }
137136
137+ /* *
138+ * Dispatch a message through the consumer client. `ec.error` also runs through the
139+ * kit-owned [defaultClient] regardless of the consumer response so unrecoverable
140+ * session errors always close checkout while still reaching `CheckoutProtocol.error`.
141+ */
138142 private fun handleClientMessage (method : String , message : String ) {
139143 log.d(LOG_TAG , " Delegating $method to client." )
140144 onMainThread {
141145 val response = client?.process(message)
142146 log.d(LOG_TAG , " client response: $response " )
143147 response?.let { sendRaw(it) }
148+ if (method == CheckoutProtocol .error.method) {
149+ // Unrecoverable ec.error is kit behavior: emit to the client, then run
150+ // the default handler so checkout is dismissed even if the client responded.
151+ defaultClient.process(message)?.let { sendRaw(it) }
152+ }
144153 }
145154 }
146155
@@ -169,6 +178,37 @@ internal class EmbeddedCheckoutProtocol(
169178 }
170179 }
171180
181+ /* *
182+ * Kit-owned client that handles delegations and kit-mandated notifications,
183+ * mirroring Swift's `defaultsClient`. Currently:
184+ * - [CheckoutProtocol.windowOpen] - launches the URI via `Intent.ACTION_VIEW`, or
185+ * returns [WindowOpenResult.Rejected] with `window_open_rejected_error` semantics.
186+ * - [CheckoutProtocol.error] - when any message carries `severity: "unrecoverable"`,
187+ * dismiss the kit via the listener. Per UCP spec, `unrecoverable` means no valid
188+ * resource exists to act on, so consumers don't have to wire dismissal in every
189+ * error handler.
190+ */
191+ private fun defaultDelegationClient (): CheckoutProtocol .Client =
192+ CheckoutProtocol .Client ()
193+ .on(CheckoutProtocol .windowOpen) { request ->
194+ when (val result = ExternalUriLauncher .launch(view.context, request.url)) {
195+ is ExternalUriLauncher .Result .Launched -> WindowOpenResult .Success
196+ is ExternalUriLauncher .Result .Rejected -> {
197+ log.d(LOG_TAG , " window.open rejected for ${request.url} : ${result.reason} " )
198+ WindowOpenResult .Rejected (reason = result.reason)
199+ }
200+ }
201+ }
202+ .on(CheckoutProtocol .error) { payload ->
203+ if (payload.messages.none { it.severity == Severity .Unrecoverable }) return @on
204+ log.d(LOG_TAG , " ec.error unrecoverable; dismissing checkout via event processor" )
205+ view.getListener().onCheckoutViewFailedWithError(
206+ ClientException (
207+ errorDescription = " Embedded checkout reported unrecoverable error." ,
208+ ),
209+ )
210+ }
211+
172212 companion object {
173213 private const val LOG_TAG = BaseWebView .ECP_LOG_TAG
174214
@@ -179,10 +219,6 @@ internal class EmbeddedCheckoutProtocol(
179219 private const val ECP_RESPONSE_GLOBAL = " EmbeddedCheckoutProtocol"
180220
181221 internal const val METHOD_READY = " ec.ready"
182- internal const val METHOD_START = " ec.start"
183- internal const val METHOD_COMPLETE = " ec.complete"
184-
185- private const val METHOD_WINDOW_OPEN_REQUEST = " ec.window.open_request"
186222
187223 // Delegations this SDK supports. Echoed back in the ec.ready response as the
188224 // intersection of checkout-accepted ∩ kit-supported. Must align with the
@@ -200,24 +236,6 @@ internal class EmbeddedCheckoutProtocol(
200236
201237 private const val CODE_PARSE_ERROR = - 32700
202238 private const val CODE_METHOD_NOT_SUPPORTED = - 32601
203-
204- /* *
205- * The kit's default [CheckoutProtocol.windowOpen] handler.
206- *
207- * Mirrors Swift's `defaultsClient`: launches the URI via `Intent.ACTION_VIEW`
208- * if any activity resolves it, otherwise returns [WindowOpenResult.Rejected]
209- * with `window_open_rejected_error` semantics.
210- */
211- internal fun defaultDelegationClient (context : Context ): CheckoutProtocol .Client =
212- CheckoutProtocol .Client ().on(CheckoutProtocol .windowOpen) { request ->
213- when (val result = ExternalUriLauncher .launch(context, request.url)) {
214- is ExternalUriLauncher .Result .Launched -> WindowOpenResult .Success
215- is ExternalUriLauncher .Result .Rejected -> {
216- log.d(LOG_TAG , " window.open rejected for ${request.url} : ${result.reason} " )
217- WindowOpenResult .Rejected (reason = result.reason)
218- }
219- }
220- }
221239 }
222240}
223241
0 commit comments