Skip to content
Open
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
32 changes: 5 additions & 27 deletions platforms/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function provided by the SDK:
```kotlin
fun presentCheckout() {
val checkoutUrl = cart.checkoutUrl
ShopifyCheckoutKit.present(checkoutUrl, context, checkoutEventProcessor)
ShopifyCheckoutKit.present(checkoutUrl, context, checkoutListener)
}
```

Expand Down Expand Up @@ -332,15 +332,10 @@ A preloaded checkout _is not_ automatically invalidated when checkout is closed.

## Monitoring the lifecycle of a checkout session

Extend the `DefaultCheckoutEventProcessor` abstract class to register callbacks for key lifecycle events during the checkout session:
Extend the `DefaultCheckoutListener` abstract class to register callbacks for key lifecycle events during the checkout session:

```kotlin
val processor = object : DefaultCheckoutEventProcessor(activity) {
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
// Called when the checkout was completed successfully by the buyer.
// Use this to update UI, reset cart state, etc.
}

val listener = object : DefaultCheckoutListener(activity) {
override fun onCheckoutCanceled() {
// Called when the checkout was canceled by the buyer.
// Note: This will also be received after closing a completed checkout
Expand All @@ -350,23 +345,6 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
// Called when the checkout encountered an error and has been aborted.
}

override fun onCheckoutLinkClicked(uri: Uri) {
// Called when the buyer clicks a link within the checkout experience:
// - email address (`mailto:`)
// - telephone number (`tel:`)
// - web (http:)
// - deep link (e.g. myapp://checkout)
// and is being directed outside the application.

// Note: to support deep links on Android 11+ using the `DefaultCheckoutEventProcessor`,
// the client app should add a queries element in its manifest declaring which apps it should interact with.
// See the MobileBuyIntegration sample's manifest for an example.
// Queries reference - https://developer.android.com/guide/topics/manifest/queries-element

// If no app can be queried to deal with the link, the processor will log a warning:
// `Unrecognized scheme for link clicked in checkout` along with the uri.
}

override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
Expand Down Expand Up @@ -395,7 +373,7 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
```

> [!Note]
> The `DefaultCheckoutEventProcessor` provides default implementations for current and future callback functions (such as `onLinkClicked()`), which can be overridden by clients wanting to change default behavior.
> The `DefaultCheckoutListener` provides default implementations for the OS-level prompt callbacks, which can be overridden by clients wanting to change default behavior. Checkout completion and external link clicks are delivered through the Embedded Checkout Protocol client instead — register a handler on `CheckoutProtocol.complete` and implement `CheckoutCommunicationClient.openExternalUrl(...)`, then pass the client as the 4th argument of `ShopifyCheckoutKit.present(...)`.

### Error handling

Expand All @@ -407,7 +385,7 @@ In the event of a checkout error occurring, the Checkout Kit _may_ attempt to re
There are some caveats to note when this scenario occurs:

1. The checkout experience may look different to buyers. Though the sheet kit will attempt to load any checkoput customizations for the storefront, there is no guarantee they will show in recovery mode.
2. The `onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent)` will be emitted with partial data. Invocations will only received the order ID via `checkoutCompletedEvent.orderDetails.id`.
2. The `CheckoutProtocol.complete` handler will be invoked with partial dataonly the order ID is guaranteed.

Should you wish to opt-out of this fallback experience entirely, you can do so by overriding `shouldRecoverFromError`. Errors given to the `onCheckoutFailed(error: CheckoutException)` lifecycle method will contain an `isRecoverable` property by default indicating whether the request should be retried or not.

Expand Down
29 changes: 13 additions & 16 deletions platforms/android/lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -665,17 +665,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 onCheckoutLinkClicked (Landroid/net/Uri;)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 @@ -758,6 +747,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/CheckoutProtocol {
public static final field INSTANCE Lcom/shopify/checkoutkit/CheckoutProtocol;
public static final field specVersion Ljava/lang/String;
Expand Down Expand Up @@ -1366,11 +1364,10 @@ 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> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Lcom/shopify/checkoutkit/LogWrapper;)V
public synthetic fun <init> (Landroid/content/Context;Lcom/shopify/checkoutkit/LogWrapper;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun onCheckoutLinkClicked (Landroid/net/Uri;)V
public fun onGeolocationPermissionsHidePrompt ()V
public fun onGeolocationPermissionsShowPrompt (Ljava/lang/String;Landroid/webkit/GeolocationPermissions$Callback;)V
public fun onPermissionRequest (Landroid/webkit/PermissionRequest;)V
Expand Down Expand Up @@ -3931,9 +3928,9 @@ public final class com/shopify/checkoutkit/ShopifyCheckoutKit {
public static final fun getConfiguration ()Lcom/shopify/checkoutkit/Configuration;
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/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 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 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 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
Loading
Loading