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
7 changes: 3 additions & 4 deletions platforms/android/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ The sample is a separate Gradle composite (`samples/MobileBuyIntegration/setting

## Key components

- **`ShopifyCheckoutKit.kt`** — the public singleton. Entry point for all consumer interactions (configure, preload, present).
- **`ShopifyCheckoutKit.kt`** — the public singleton. Entry point for all consumer interactions (configure, present).
- **`CheckoutDialog.kt`** — the dialog that hosts the WebView, including the progress indicator and checkout error coordination.
- **`CheckoutWebView.kt`** — primary WebView. Holds the URL-keyed cache with a **5-minute preload TTL**; instruments page loads; routes bridge messages.
- **`CheckoutWebView.kt`** — primary WebView. Instruments page loads; routes bridge messages.
- **`BaseWebView.kt`** — abstract base class. Any new WebView variant must extend this so shared configuration (JS interface name, user agent suffix, client handling) is consistent.
- **`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). Any config change clears the WebView cache.
- **`Configuration.kt`** — runtime config container (color scheme, log level).
- **`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.

Expand Down Expand Up @@ -95,5 +95,4 @@ Publishing goes through GitHub Releases → the repo-root `.github/workflows/and
- **Library Kotlin version pin.** Consumer compatibility floor; any migration is a deliberate major-version decision.
- **`minSdk` / JVM target.** Same story.
- **`CheckoutBridge.SCHEMA_VERSION`.** Cross-team contract with the web checkout — changing it without coordination breaks the bridge.
- **Preload TTL (5 minutes).** Performance-sensitive; has been tuned. Don't tweak without a reason.
- **`-Xexplicit-api=strict`.** Removing this would let implicit public declarations ship; keeping it is a consumer-protection invariant.
25 changes: 4 additions & 21 deletions platforms/android/lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1089,21 +1089,18 @@ public final class com/shopify/checkoutkit/ColorsBuilder {
public final class com/shopify/checkoutkit/Configuration {
public fun <init> ()V
public final fun component1 ()Lcom/shopify/checkoutkit/ColorScheme;
public final fun component2 ()Lcom/shopify/checkoutkit/Preloading;
public final fun component3 ()Lcom/shopify/checkoutkit/Platform;
public final fun component4 ()Lcom/shopify/checkoutkit/LogLevel;
public final fun copy (Lcom/shopify/checkoutkit/ColorScheme;Lcom/shopify/checkoutkit/Preloading;Lcom/shopify/checkoutkit/Platform;Lcom/shopify/checkoutkit/LogLevel;)Lcom/shopify/checkoutkit/Configuration;
public static synthetic fun copy$default (Lcom/shopify/checkoutkit/Configuration;Lcom/shopify/checkoutkit/ColorScheme;Lcom/shopify/checkoutkit/Preloading;Lcom/shopify/checkoutkit/Platform;Lcom/shopify/checkoutkit/LogLevel;ILjava/lang/Object;)Lcom/shopify/checkoutkit/Configuration;
public final fun component2 ()Lcom/shopify/checkoutkit/Platform;
public final fun component3 ()Lcom/shopify/checkoutkit/LogLevel;
public final fun copy (Lcom/shopify/checkoutkit/ColorScheme;Lcom/shopify/checkoutkit/Platform;Lcom/shopify/checkoutkit/LogLevel;)Lcom/shopify/checkoutkit/Configuration;
public static synthetic fun copy$default (Lcom/shopify/checkoutkit/Configuration;Lcom/shopify/checkoutkit/ColorScheme;Lcom/shopify/checkoutkit/Platform;Lcom/shopify/checkoutkit/LogLevel;ILjava/lang/Object;)Lcom/shopify/checkoutkit/Configuration;
public fun equals (Ljava/lang/Object;)Z
public final fun getColorScheme ()Lcom/shopify/checkoutkit/ColorScheme;
public final fun getLogLevel ()Lcom/shopify/checkoutkit/LogLevel;
public final fun getPlatform ()Lcom/shopify/checkoutkit/Platform;
public final fun getPreloading ()Lcom/shopify/checkoutkit/Preloading;
public fun hashCode ()I
public final fun setColorScheme (Lcom/shopify/checkoutkit/ColorScheme;)V
public final fun setLogLevel (Lcom/shopify/checkoutkit/LogLevel;)V
public final fun setPlatform (Lcom/shopify/checkoutkit/Platform;)V
public final fun setPreloading (Lcom/shopify/checkoutkit/Preloading;)V
public fun toString ()Ljava/lang/String;
}

Expand Down Expand Up @@ -3661,19 +3658,6 @@ public final class com/shopify/checkoutkit/PostalAddress$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class com/shopify/checkoutkit/Preloading {
public fun <init> ()V
public fun <init> (Z)V
public synthetic fun <init> (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Z
public final fun copy (Z)Lcom/shopify/checkoutkit/Preloading;
public static synthetic fun copy$default (Lcom/shopify/checkoutkit/Preloading;ZILjava/lang/Object;)Lcom/shopify/checkoutkit/Preloading;
public fun equals (Ljava/lang/Object;)Z
public final fun getEnabled ()Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/shopify/checkoutkit/PurpleSelectedPaymentInstrument {
public static final field Companion Lcom/shopify/checkoutkit/PurpleSelectedPaymentInstrument$Companion;
public fun <init> (Lcom/shopify/checkoutkit/BillingAddressClass;Lcom/shopify/checkoutkit/CredentialClass;Lkotlinx/serialization/json/JsonObject;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V
Expand Down Expand Up @@ -3893,7 +3877,6 @@ public final class com/shopify/checkoutkit/ShopifyCheckoutKit {
public static final field version Ljava/lang/String;
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/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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,6 @@ internal fun BaseWebView.removeFromParent() {
val parent = this.parent
if (parent is ViewGroup) {
log.d("BaseWebView", "Existing parent found for WebView, removing.")
// Ensure view is not destroyed when removing from parent
CheckoutWebViewContainer.retainCacheEntry = RetainCacheEntry.YES
parent.removeView(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,10 @@ internal class CheckoutDialog(
// properly into the fields. To be investigated further.
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)

log.d(LOG_TAG, "Finding or creating new WebView.")
val checkoutWebView = CheckoutWebView.cacheableCheckoutView(
checkoutUrl,
context,
)
log.d(LOG_TAG, "Creating new WebView.")
val checkoutWebView = CheckoutWebView(context as Context).apply {
loadCheckout(checkoutUrl)
}

checkoutWebView.onResume()
log.d(LOG_TAG, "Setting listener on WebView.")
Expand Down Expand Up @@ -116,7 +115,6 @@ internal class CheckoutDialog(
onBackPressedDispatcher.addCallback(backNavigationCallback)
setOnCancelListener {
log.d(LOG_TAG, "Cancel listener invoked, invoking onCheckoutCanceled.")
CheckoutWebViewContainer.retainCacheEntry = RetainCacheEntry.IF_NOT_STALE
checkoutListener.onCheckoutCanceled()
}

Expand Down Expand Up @@ -198,8 +196,7 @@ internal class CheckoutDialog(
}

internal fun closeCheckoutDialogWithError(exception: CheckoutException) {
log.d(LOG_TAG, "Closing dialog with error, marking cache entry stale, calling onCheckoutFailed.")
CheckoutWebView.markCacheEntryStale()
log.d(LOG_TAG, "Closing dialog with error, calling onCheckoutFailed.")
checkoutListener.onCheckoutFailed(exception)
dismiss()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,11 @@ import android.os.Looper
import android.util.AttributeSet
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.activity.ComponentActivity
import com.shopify.checkoutkit.ShopifyCheckoutKit.log
import java.util.concurrent.CountDownLatch
import kotlin.math.abs
import kotlin.time.Duration.Companion.minutes

internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = null) :
BaseWebView(context, attributeSet) {

var isPreload = false

private val checkoutBridge = CheckoutBridge(CheckoutWebViewListener(NoopCheckoutListener()))
private val embeddedCheckoutProtocol = EmbeddedCheckoutProtocol(this)
private var loadComplete = false
Expand Down Expand Up @@ -81,13 +75,11 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
removeJavascriptInterface(EmbeddedCheckoutProtocol.INTERFACE_NAME)
}

fun loadCheckout(url: String, isPreload: Boolean) {
log.d(LOG_TAG, "Loading checkout with url $url. IsPreload: $isPreload.")
this.isPreload = isPreload
fun loadCheckout(url: String) {
log.d(LOG_TAG, "Loading checkout with url $url.")
Handler(Looper.getMainLooper()).post {
val ecpUrl = url.appendEcpParams(specVersion = CheckoutProtocol.specVersion)
val headers = if (isPreload) mutableMapOf("Shopify-Purpose" to "prefetch") else mutableMapOf()
loadUrl(ecpUrl, headers)
loadUrl(ecpUrl)
}
}

Expand Down Expand Up @@ -126,112 +118,5 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
companion object {
private const val LOG_TAG = "CheckoutWebView"
private const val JAVASCRIPT_INTERFACE_NAME = "android"

internal var cacheEntry: CheckoutWebViewCacheEntry? = null
internal var cacheClock = CheckoutWebViewCacheClock()

fun markCacheEntryStale() {
cacheEntry = cacheEntry?.copy(timeout = -1)
}

fun clearCache(newEntry: CheckoutWebViewCacheEntry? = null) = cacheEntry?.let {
Handler(Looper.getMainLooper()).post {
it.view.destroy()
cacheEntry = newEntry
}
}

fun cacheableCheckoutView(
url: String,
activity: ComponentActivity,
isPreload: Boolean = false,
): CheckoutWebView {
var view: CheckoutWebView? = null
val countDownLatch = CountDownLatch(1)

activity.runOnUiThread {
view = fetchView(url, activity, isPreload)
countDownLatch.countDown()
}

countDownLatch.await()

return view!!
}

private fun fetchView(
url: String,
activity: ComponentActivity,
isPreload: Boolean,
): CheckoutWebView {
val preloadingEnabled = ShopifyCheckoutKit.configuration.preloading.enabled
log.d(
LOG_TAG,
"Fetch view called for url $url. Is preload: $isPreload. Preloading enabled: $preloadingEnabled."
)
if (!preloadingEnabled || cacheEntry?.isValid(url) != true) {
log.d(LOG_TAG, "Constructing new CheckoutWebView and calling loadCheckout.")
val view = CheckoutWebView(activity as Context).apply {
loadCheckout(url, isPreload)
if (isPreload) {
// Pauses processing that can be paused safely (e.g. geolocation, animations), but not JavaScript / network requests
// https://developer.android.com/reference/android/webkit/WebView#onPause()
onPause()
}
}

setCacheEntry(
CheckoutWebViewCacheEntry(
key = url,
view = view,
clock = cacheClock,
timeout = if (isPreload && preloadingEnabled) 5.minutes.inWholeMilliseconds else 0,
)
)

return view
}

log.d(LOG_TAG, "Returning previously cached view.")
return cacheEntry!!.view
}

private fun setCacheEntry(cacheEntry: CheckoutWebViewCacheEntry) {
if (this.cacheEntry == null) {
log.d(
LOG_TAG,
"Caching CheckoutWebView with TTL: ${cacheEntry.timeout} (note: a TTL of 0 is equivalent to not caching)."
)

this.cacheEntry = cacheEntry
} else {
log.d(LOG_TAG, "Clearing WebView cache and destroying cached view.")
clearCache(cacheEntry)
}
}
}

internal data class CheckoutWebViewCacheEntry(
val key: String,
val view: CheckoutWebView,
val clock: CheckoutWebViewCacheClock,
val timeout: Long,
) {
private val timestamp = clock.currentTimeMillis()

fun isValid(key: String): Boolean {
return key == cacheEntry!!.key && !cacheEntry!!.isStale
}

internal val isStale: Boolean
get() {
val staleResult = abs(timestamp - clock.currentTimeMillis()) >= timeout
log.d(LOG_TAG, "Checking cache entry staleness. Is stale: $staleResult.")
return staleResult
}
}

internal class CheckoutWebViewCacheClock {
fun currentTimeMillis(): Long = System.currentTimeMillis()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import android.webkit.WebView
internal class CheckoutWebViewListener(
private val listener: CheckoutListener,
private val toggleHeader: (Boolean) -> Unit = {},
private val closeCheckoutDialogWithError: (CheckoutException) -> Unit = { CheckoutWebView.clearCache() },
private val closeCheckoutDialogWithError: (CheckoutException) -> Unit = {},
private val setProgressBarVisibility: (Int) -> Unit = {},
private val updateProgressBarPercentage: (Int) -> Unit = {},
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,14 @@ package com.shopify.checkoutkit
/**
* Configuration for Shopify Checkout Kit.
*
* Allows:
* - Enabling/disabling preloading,
* - Specifying the colorScheme that should be used for checkout.
* Allows specifying the colorScheme that should be used for checkout.
*/
public data class Configuration internal constructor(
var colorScheme: ColorScheme = ColorScheme.Automatic(),
var preloading: Preloading = Preloading(),
var platform: Platform? = null,
var logLevel: LogLevel = LogLevel.WARN,
)

/**
* Configuration related to preloading.
*
* Initially allows toggling the preloading feature.
*/
public data class Preloading(
val enabled: Boolean = true
)

public enum class LogLevel {
DEBUG, WARN, ERROR
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,7 @@ internal class EmbeddedCheckoutProtocol(
}

private fun handleComplete(message: String) {
// Cache invalidation on completion is a kit invariant — independent of whether
// a merchant client is attached. Mark stale before delegating so a completed
// checkout is never reused from cache on the next present(...).
log.d(LOG_TAG, "Handling $METHOD_COMPLETE: marking cache stale and bubbling up.")
CheckoutWebView.markCacheEntryStale()
log.d(LOG_TAG, "Handling $METHOD_COMPLETE: bubbling up.")
onMainThread {
client?.process(message)
}
Expand Down
Loading
Loading