Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion platforms/android/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ The sample is a separate Gradle composite (`samples/MobileBuyIntegration/setting
- **`FallbackWebView.kt`** — minimal WebView swapped in during error recovery when the primary path fails.
- **`CheckoutBridge.kt`** — the JS ↔ native bridge. `SCHEMA_VERSION` is a cross-boundary contract with the web checkout team; bumping it requires coordination with them.
- **`Configuration.kt`** — runtime config container (color scheme, preload enable, log level, error recovery policy). Any config change clears the WebView cache.
- **`CheckoutEventProcessor.kt`** + **`DefaultCheckoutEventProcessor`** — consumer-implemented lifecycle interface (completion, failure, permission prompts, link clicks). Changes here are consumer API changes.
- **`CheckoutListener.kt`** + **`DefaultCheckoutListener`** — consumer-implemented lifecycle interface (failure, cancellation, permission prompts, file chooser). Changes here are consumer API changes.
- **`CheckoutPresentation.kt`** — Kotlin-first builder for per-presentation callbacks (`onFail`, `onCancel`, browser/system hooks, ECP `connect(...)`). Builds a `DefaultCheckoutListener` internally.

## Testing patterns

Expand Down
8 changes: 4 additions & 4 deletions platforms/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,10 @@ ShopifyCheckoutKit.present(checkoutUrl, activity) {
```

If you prefer a reusable object, or are integrating from Java, extend
`DefaultCheckoutEventProcessor` and pass it to the existing `present(...)` overload:
`DefaultCheckoutListener` and pass it to the existing `present(...)` overload:

```kotlin
val processor = object : DefaultCheckoutEventProcessor() {
val listener = object : DefaultCheckoutListener() {
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
Expand All @@ -324,11 +324,11 @@ val processor = object : DefaultCheckoutEventProcessor() {
}
}

ShopifyCheckoutKit.present(checkoutUrl, context, processor)
ShopifyCheckoutKit.present(checkoutUrl, context, listener)
```

> [!Note]
> The `DefaultCheckoutEventProcessor` overload remains available for reusable or Java-facing
> The `DefaultCheckoutListener` overload remains available for reusable or Java-facing
> integrations and provides default implementations for optional browser/system callbacks.

### Error handling
Expand Down
27 changes: 13 additions & 14 deletions platforms/android/lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -664,16 +664,6 @@ public final class com/shopify/checkoutkit/CheckoutError$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public abstract interface class com/shopify/checkoutkit/CheckoutEventProcessor {
public abstract fun onCheckoutCanceled ()V
public abstract fun onCheckoutCompleted (Lcom/shopify/checkoutkit/lifecycleevents/CheckoutCompletedEvent;)V
public abstract fun onCheckoutFailed (Lcom/shopify/checkoutkit/CheckoutException;)V
public abstract fun onGeolocationPermissionsHidePrompt ()V
public abstract fun onGeolocationPermissionsShowPrompt (Ljava/lang/String;Landroid/webkit/GeolocationPermissions$Callback;)V
public abstract fun onPermissionRequest (Landroid/webkit/PermissionRequest;)V
public abstract fun onShowFileChooser (Landroid/webkit/WebView;Landroid/webkit/ValueCallback;Landroid/webkit/WebChromeClient$FileChooserParams;)Z
}

public abstract class com/shopify/checkoutkit/CheckoutException : java/lang/Exception {
public static final field Companion Lcom/shopify/checkoutkit/CheckoutException$Companion;
public synthetic fun <init> (ILjava/lang/String;Ljava/lang/String;ZLkotlinx/serialization/internal/SerializationConstructorMarker;)V
Expand Down Expand Up @@ -756,6 +746,15 @@ public final class com/shopify/checkoutkit/CheckoutLineItem$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public abstract interface class com/shopify/checkoutkit/CheckoutListener {
public abstract fun onCheckoutCanceled ()V
public abstract fun onCheckoutFailed (Lcom/shopify/checkoutkit/CheckoutException;)V
public abstract fun onGeolocationPermissionsHidePrompt ()V
public abstract fun onGeolocationPermissionsShowPrompt (Ljava/lang/String;Landroid/webkit/GeolocationPermissions$Callback;)V
public abstract fun onPermissionRequest (Landroid/webkit/PermissionRequest;)V
public abstract fun onShowFileChooser (Landroid/webkit/WebView;Landroid/webkit/ValueCallback;Landroid/webkit/WebChromeClient$FileChooserParams;)Z
}

public final class com/shopify/checkoutkit/CheckoutPresentation {
public final fun connect (Lcom/shopify/checkoutkit/CheckoutCommunicationClient;)V
public final fun onCancel (Lkotlin/jvm/functions/Function0;)V
Expand Down Expand Up @@ -1372,7 +1371,7 @@ public final class com/shopify/checkoutkit/CredentialResult$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public abstract class com/shopify/checkoutkit/DefaultCheckoutEventProcessor : com/shopify/checkoutkit/CheckoutEventProcessor {
public abstract class com/shopify/checkoutkit/DefaultCheckoutListener : com/shopify/checkoutkit/CheckoutListener {
public fun <init> ()V
public fun onGeolocationPermissionsHidePrompt ()V
public fun onGeolocationPermissionsShowPrompt (Ljava/lang/String;Landroid/webkit/GeolocationPermissions$Callback;)V
Expand Down Expand Up @@ -3933,10 +3932,10 @@ public final class com/shopify/checkoutkit/ShopifyCheckoutKit {
public static final fun configure (Lcom/shopify/checkoutkit/ConfigurationUpdater;)V
public static final fun getConfiguration ()Lcom/shopify/checkoutkit/Configuration;
public static final fun invalidate ()V
public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutEventProcessor;)Lcom/shopify/checkoutkit/CheckoutKitDialog;
public static final fun present (Ljava/lang/String;Landroidx/activity/ComponentActivity;Lcom/shopify/checkoutkit/DefaultCheckoutEventProcessor;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/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 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/DefaultCheckoutEventProcessor;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/CheckoutCommunicationClient;ILjava/lang/Object;)Lcom/shopify/checkoutkit/CheckoutKitDialog;
}

public final class com/shopify/checkoutkit/Signals {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
configureWebView()
}

abstract fun getEventProcessor(): CheckoutWebViewEventProcessor
abstract fun getListener(): CheckoutWebViewListener
abstract val recoverErrors: Boolean

private fun configureWebView() {
Expand All @@ -77,31 +77,31 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
log.d(LOG_TAG, "On progress change called. New progress $newProgress.")
getEventProcessor().updateProgressBar(newProgress)
getListener().updateProgressBar(newProgress)
}

override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
log.d(LOG_TAG, "onGeolocationPermissionsShowPrompt called, origin $origin, invoking eventProcessor callback.")
getEventProcessor().onGeolocationPermissionsShowPrompt(origin, callback)
log.d(LOG_TAG, "onGeolocationPermissionsShowPrompt called, origin $origin, invoking listener callback.")
getListener().onGeolocationPermissionsShowPrompt(origin, callback)
}

override fun onGeolocationPermissionsHidePrompt() {
log.d(LOG_TAG, "onGeolocationPermissionsHidePrompt called, invoking eventProcessor callback.")
getEventProcessor().onGeolocationPermissionsHidePrompt()
log.d(LOG_TAG, "onGeolocationPermissionsHidePrompt called, invoking listener callback.")
getListener().onGeolocationPermissionsHidePrompt()
}

override fun onPermissionRequest(request: PermissionRequest) {
log.d(LOG_TAG, "onPermissionRequest called $request, invoking eventProcessor callback.")
getEventProcessor().onPermissionRequest(request)
log.d(LOG_TAG, "onPermissionRequest called $request, invoking listener callback.")
getListener().onPermissionRequest(request)
}

override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams,
): Boolean {
log.d(LOG_TAG, "onShowFileChooser called, invoking eventProcessor callback.")
return getEventProcessor().onShowFileChooser(webView, filePathCallback, fileChooserParams)
log.d(LOG_TAG, "onShowFileChooser called, invoking listener callback.")
return getListener().onShowFileChooser(webView, filePathCallback, fileChooserParams)
}
}
isHorizontalScrollBarEnabled = false
Expand Down Expand Up @@ -144,8 +144,8 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !detail.didCrash()) {
// Renderer was killed because system ran out of memory.
log.d(LOG_TAG, "onRenderProcessGone called, calling onCheckoutFailedWithError")
val eventProcessor = getEventProcessor()
eventProcessor.onCheckoutViewFailedWithError(
val listener = getListener()
listener.onCheckoutViewFailedWithError(
CheckoutKitException(
errorDescription = "Render process gone.",
errorCode = CheckoutKitException.RENDER_PROCESS_GONE,
Expand Down Expand Up @@ -207,11 +207,11 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
LOG_TAG,
"Handling error for main frame. URL: ${request.url}, errorCode: $errorCode, errorDescription: $errorDescription"
)
val processor = getEventProcessor()
val listener = getListener()
when {
errorCode == HTTP_GONE -> {
log.d(LOG_TAG, "Failing with cart expired. Recoverable: false")
processor.onCheckoutViewFailedWithError(
listener.onCheckoutViewFailedWithError(
CheckoutExpiredException(
isRecoverable = false,
errorCode = CheckoutExpiredException.CART_EXPIRED
Expand All @@ -222,7 +222,7 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
else -> {
val recoverable = isRecoverable(errorCode)
log.d(LOG_TAG, "Failing with other error. Code: $errorCode. Recoverable $recoverable")
processor.onCheckoutViewFailedWithError(
listener.onCheckoutViewFailedWithError(
HttpException(
errorDescription = errorDescription,
statusCode = errorCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,26 @@
package com.shopify.checkoutkit

import android.webkit.JavascriptInterface
import com.shopify.checkoutkit.CheckoutBridge.CheckoutWebOperation.COMPLETED
import com.shopify.checkoutkit.CheckoutBridge.CheckoutWebOperation.ERROR
import com.shopify.checkoutkit.CheckoutBridge.CheckoutWebOperation.MODAL
import com.shopify.checkoutkit.ShopifyCheckoutKit.log
import com.shopify.checkoutkit.errorevents.CheckoutErrorDecoder
import com.shopify.checkoutkit.lifecycleevents.CheckoutCompletedEventDecoder
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

internal class CheckoutBridge(
private var eventProcessor: CheckoutWebViewEventProcessor,
private var listener: CheckoutWebViewListener,
private val decoder: Json = Json { ignoreUnknownKeys = true },
private val checkoutCompletedEventDecoder: CheckoutCompletedEventDecoder = CheckoutCompletedEventDecoder(
decoder,
log
),
private val checkoutErrorDecoder: CheckoutErrorDecoder = CheckoutErrorDecoder(decoder, log),
) {

fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
this.eventProcessor = eventProcessor
fun setListener(listener: CheckoutWebViewListener) {
this.listener = listener
}

fun getEventProcessor(): CheckoutWebViewEventProcessor = this.eventProcessor
fun getListener(): CheckoutWebViewListener = this.listener

enum class CheckoutWebOperation(val key: String) {
COMPLETED("completed"),
MODAL("checkoutBlockingEvent"),
ERROR("error");

Expand All @@ -69,23 +62,13 @@ internal class CheckoutBridge(
val decodedMsg = decoder.decodeFromString<WebToSdkEvent>(message)

when (CheckoutWebOperation.fromKey(decodedMsg.name)) {
COMPLETED -> {
log.d(LOG_TAG, "Received Completed message. Attempting to decode.")
checkoutCompletedEventDecoder.decode(decodedMsg).let { event ->
log.d(LOG_TAG, "Decoded message $event.")
onMainThread {
eventProcessor.onCheckoutViewComplete(event)
}
}
}

MODAL -> {
log.d(LOG_TAG, "Received Modal message.")
val modalVisible = decodedMsg.body.toBooleanStrictOrNull()
modalVisible?.let {
log.d(LOG_TAG, "Modal visible $it")
onMainThread {
eventProcessor.onCheckoutViewModalToggled(modalVisible)
listener.onCheckoutViewModalToggled(modalVisible)
}
}
}
Expand All @@ -95,7 +78,7 @@ internal class CheckoutBridge(
checkoutErrorDecoder.decode(decodedMsg)?.let { exception ->
log.d(LOG_TAG, "Decoded message $exception.")
onMainThread {
eventProcessor.onCheckoutViewFailedWithError(exception)
listener.onCheckoutViewFailedWithError(exception)
}
}
}
Expand All @@ -105,7 +88,7 @@ internal class CheckoutBridge(
} catch (e: Exception) {
log.d(LOG_TAG, "Failed to decode message with error: $e. Calling onCheckoutFailedWithError")
onMainThread {
eventProcessor.onCheckoutViewFailedWithError(
listener.onCheckoutViewFailedWithError(
CheckoutKitException(
errorDescription = "Error decoding message from checkout.",
errorCode = CheckoutKitException.ERROR_RECEIVING_MESSAGE_FROM_CHECKOUT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import com.shopify.checkoutkit.ShopifyCheckoutKit.log

internal class CheckoutDialog(
private val checkoutUrl: String,
private val checkoutEventProcessor: CheckoutEventProcessor,
private val checkoutListener: CheckoutListener,
context: Context,
private val communicationClient: CheckoutCommunicationClient? = null,
) : ComponentDialog(context) {
Expand Down Expand Up @@ -90,8 +90,8 @@ internal class CheckoutDialog(
)

checkoutWebView.onResume()
log.d(LOG_TAG, "Setting event processor on WebView.")
checkoutWebView.setEventProcessor(eventProcessor())
log.d(LOG_TAG, "Setting listener on WebView.")
checkoutWebView.setListener(webViewListener())
log.d(LOG_TAG, "Setting communication client on WebView.")
checkoutWebView.setClient(communicationClient)

Expand Down Expand Up @@ -119,7 +119,7 @@ internal class CheckoutDialog(
setOnCancelListener {
log.d(LOG_TAG, "Cancel listener invoked, invoking onCheckoutCanceled.")
CheckoutWebViewContainer.retainCacheEntry = RetainCacheEntry.IF_NOT_STALE
checkoutEventProcessor.onCheckoutCanceled()
checkoutListener.onCheckoutCanceled()
}

setOnDismissListener {
Expand Down Expand Up @@ -203,7 +203,7 @@ internal class CheckoutDialog(
log.d(LOG_TAG, "Closing dialog with error, marking cache entry stale, calling onCheckoutFailed.")
recoveryAttemptCount++
CheckoutWebView.markCacheEntryStale()
checkoutEventProcessor.onCheckoutFailed(exception)
checkoutListener.onCheckoutFailed(exception)

val isOneTimeUseUrl = this.checkoutUrl.isOneTimeUse()
val shouldRecover = ShopifyCheckoutKit.configuration.errorRecovery.shouldRecoverFromError(exception)
Expand Down Expand Up @@ -233,16 +233,16 @@ internal class CheckoutDialog(
addWebViewToContainer(
ShopifyCheckoutKit.configuration.colorScheme,
FallbackWebView(context).apply {
setEventProcessor(eventProcessor())
setListener(webViewListener())
loadUrl(checkoutUrl)
}
)
return true
}

private fun eventProcessor(): CheckoutWebViewEventProcessor {
return CheckoutWebViewEventProcessor(
eventProcessor = checkoutEventProcessor,
private fun webViewListener(): CheckoutWebViewListener {
return CheckoutWebViewListener(
listener = checkoutListener,
toggleHeader = ::toggleHeader,
closeCheckoutDialogWithError = ::closeCheckoutDialogWithError,
setProgressBarVisibility = ::setProgressBarVisibility,
Expand Down
Loading
Loading