Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,21 +664,24 @@ see [guidelines and instructions](.github/CONTRIBUTING.md).

Shopify's Checkout Kit is provided under an [MIT License](LICENSE).


----
---

## Migration

### Currently Lost
### Currently Lost

#### Outbound

- presented
- instrumentation (deprecated)

#### Inbound

- pixels
- blocking
- error

#### Bugs and stuff

- evaling js... relying on cache - not safe
- not sending onError yet
13 changes: 7 additions & 6 deletions lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit.log
import java.net.HttpURLConnection.HTTP_GONE

@SuppressLint("SetJavaScriptEnabled")
internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet? = null) :
public abstract class BaseWebView(context: Context, attributeSet: AttributeSet? = null) :
WebView(context, attributeSet) {

internal val authenticationTracker = AuthenticationTracker()
Expand All @@ -58,10 +58,10 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
configureWebView()
}

abstract fun getEventProcessor(): CheckoutWebViewEventProcessor
abstract val recoverErrors: Boolean
internal abstract fun getEventProcessor(): CheckoutWebViewEventProcessor
internal abstract val recoverErrors: Boolean

open val checkoutOptions: CheckoutOptions? = null
protected open val checkoutOptions: CheckoutOptions? = null

private fun configureWebView() {
visibility = VISIBLE
Expand Down Expand Up @@ -123,7 +123,7 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
return super.onKeyDown(keyCode, event)
}

open inner class BaseWebViewClient : WebViewClient() {
internal open inner class BaseWebViewClient : WebViewClient() {
init {
if (BuildConfig.DEBUG) {
log.d(LOG_TAG, "Setting web contents debugging enabled.")
Expand Down Expand Up @@ -246,8 +246,9 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
}
}

companion object {
internal companion object {
private const val LOG_TAG = "BaseWebView"

private const val TOO_MANY_REQUESTS = 429
private val CLIENT_ERROR = 400..499
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public data class CheckoutAddressChangeRequestedEventData(
public class CheckoutAddressChangeRequestedEvent internal constructor(
internal val message: CheckoutMessageParser.JSONRPCMessage.AddressChangeRequested,
) {
public val id: String? get() = message.id
public val addressType: String get() = message.addressType
public val selectedAddress: CartDeliveryAddressInput? get() = message.selectedAddress

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import android.webkit.JavascriptInterface
import android.webkit.WebView
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit.log
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.lang.ref.WeakReference

Expand All @@ -37,6 +38,7 @@ internal class CheckoutBridge(
) {

private var webViewRef: WeakReference<WebView>? = null
private val pendingEvents = mutableMapOf<String, CheckoutMessageParser.JSONRPCMessage>()
Comment thread
kieran-osgood-shopify marked this conversation as resolved.

fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
this.eventProcessor = eventProcessor
Expand All @@ -48,6 +50,29 @@ internal class CheckoutBridge(
this.webViewRef = if (webView != null) WeakReference(webView) else null
}

/**
* Respond to an RPC event with the given response data
* @param eventId The ID of the event to respond to
* @param responseData The JSON response data to send back
*/
fun respondToEvent(eventId: String, responseData: String) {
val event = pendingEvents[eventId]
if (event is CheckoutMessageParser.JSONRPCMessage.AddressChangeRequested) {
try {
// Parse the response data as DeliveryAddressChangePayload
val jsonParser = Json { ignoreUnknownKeys = true }
val payload = jsonParser.decodeFromString<DeliveryAddressChangePayload>(responseData)
event.respondWith(payload)
pendingEvents.remove(eventId)
log.d(LOG_TAG, "Successfully responded to event $eventId")
} catch (e: Exception) {
log.e(LOG_TAG, "Failed to parse response data for event $eventId: ${e.message}")
}
} else {
log.w(LOG_TAG, "No pending event found with ID $eventId")
}
}

// Allows Web to postMessages back to the SDK
@Suppress("SwallowedException")
@JavascriptInterface
Expand All @@ -61,6 +86,10 @@ internal class CheckoutBridge(
webViewRef?.get()?.let { webView ->
checkoutMessage.setWebView(webView)
}
// Store the event for potential React Native response
checkoutMessage.id?.let { id ->
pendingEvents[id] = checkoutMessage
}
onMainThread {
eventProcessor.onAddressChangeRequested(checkoutMessage.toEvent())
}
Expand Down
64 changes: 46 additions & 18 deletions lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ import java.util.concurrent.CountDownLatch
import kotlin.math.abs
import kotlin.time.Duration.Companion.minutes

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

override val recoverErrors = true
var isPreload = false
override var checkoutOptions: CheckoutOptions? = null
internal override val recoverErrors: Boolean = true
private var isPreload: Boolean = false
protected override var checkoutOptions: CheckoutOptions? = null

private val checkoutBridge = CheckoutBridge(CheckoutWebViewEventProcessor(NoopEventProcessor()))
private var loadComplete = false
Expand All @@ -62,19 +62,38 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
checkoutBridge.setWebView(this)
}

fun hasFinishedLoading() = loadComplete
public fun hasFinishedLoading(): Boolean = loadComplete

fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
public fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
log.d(LOG_TAG, "Setting event processor $eventProcessor.")
checkoutBridge.setEventProcessor(eventProcessor)
}

fun notifyPresented() {
public fun notifyPresented() {
log.d(LOG_TAG, "Notify presented called.")
presented = true
}

override fun getEventProcessor(): CheckoutWebViewEventProcessor {
/**
* Reload the current checkout page
*/
public override fun reload() {
log.d(LOG_TAG, "Reloading checkout")
super.reload()
}

/**
* Respond to an RPC event (e.g. address change, payment change)
* @param eventId The ID of the event to respond to
* @param responseData The response data to send back
*/
public fun respondToEvent(eventId: String, responseData: String) {
log.d(LOG_TAG, "Responding to event $eventId with data: $responseData")
// Delegate to the CheckoutBridge to handle the response
checkoutBridge.respondToEvent(eventId, responseData)
}

internal override fun getEventProcessor(): CheckoutWebViewEventProcessor {
return checkoutBridge.getEventProcessor()
}

Expand All @@ -92,7 +111,7 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
checkoutBridge.setWebView(null)
}

fun loadCheckout(url: String, isPreload: Boolean, options: CheckoutOptions? = null) {
public fun loadCheckout(url: String, isPreload: Boolean, options: CheckoutOptions? = null) {
log.d(LOG_TAG, "Loading checkout with url $url. IsPreload: $isPreload.")
this.isPreload = isPreload
this.checkoutOptions = options
Expand All @@ -110,7 +129,7 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
}
}

inner class CheckoutWebViewClient : BaseWebView.BaseWebViewClient() {
internal inner class CheckoutWebViewClient : BaseWebView.BaseWebViewClient() {

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
Expand Down Expand Up @@ -173,26 +192,35 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
}
}

companion object {
public companion object {
private const val LOG_TAG = "CheckoutWebView"
private const val OPEN_EXTERNALLY_PARAM = "open_externally"
private const val JAVASCRIPT_INTERFACE_NAME = "EmbeddedCheckoutProtocolConsumer"

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

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

fun clearCache(newEntry: CheckoutWebViewCacheEntry? = null) = cacheEntry?.let {
Handler(Looper.getMainLooper()).post {
it.view.destroy()
cacheEntry = newEntry
/**
* Clear the cached CheckoutWebView
*/
public fun clearCache() {
clearCacheInternal(null)
}

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

fun cacheableCheckoutView(
public fun cacheableCheckoutView(
url: String,
activity: ComponentActivity,
isPreload: Boolean = false,
Expand Down Expand Up @@ -256,7 +284,7 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
this.cacheEntry = cacheEntry
} else {
log.d(LOG_TAG, "Clearing WebView cache and destroying cached view.")
clearCache(cacheEntry)
clearCacheInternal(cacheEntry)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,90 +38,90 @@ import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
* Event processor that can handle events internally, delegate to the CheckoutEventProcessor
* passed into ShopifyCheckoutSheetKit.present(), or preprocess arguments and then delegate
*/
internal class CheckoutWebViewEventProcessor(
public class CheckoutWebViewEventProcessor(
private val eventProcessor: CheckoutEventProcessor,
private val toggleHeader: (Boolean) -> Unit = {},
private val closeCheckoutDialogWithError: (CheckoutException) -> Unit = { CheckoutWebView.clearCache() },
private val closeCheckoutDialogWithError: (CheckoutException) -> Unit = { CheckoutWebView.clearCacheInternal() },
private val setProgressBarVisibility: (Int) -> Unit = {},
private val updateProgressBarPercentage: (Int) -> Unit = {},
) {
fun onCheckoutViewComplete(checkoutCompleteEvent: CheckoutCompleteEvent) {
internal fun onCheckoutViewComplete(checkoutCompleteEvent: CheckoutCompleteEvent) {
log.d(LOG_TAG, "Clearing WebView cache after checkout completion.")
CheckoutWebView.markCacheEntryStale()

log.d(LOG_TAG, "Calling onCheckoutCompleted $checkoutCompleteEvent.")
eventProcessor.onCheckoutCompleted(checkoutCompleteEvent)
}

fun onCheckoutViewModalToggled(modalVisible: Boolean) {
internal fun onCheckoutViewModalToggled(modalVisible: Boolean) {
onMainThread {
toggleHeader(modalVisible)
}
}

fun onCheckoutViewLinkClicked(uri: Uri) {
internal fun onCheckoutViewLinkClicked(uri: Uri) {
log.d(LOG_TAG, "Calling onCheckoutLinkClicked.")
eventProcessor.onCheckoutLinkClicked(uri)
}

fun onCheckoutViewFailedWithError(error: CheckoutException) {
internal fun onCheckoutViewFailedWithError(error: CheckoutException) {
onMainThread {
closeCheckoutDialogWithError(error)
}
}

fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
internal fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
return eventProcessor.onGeolocationPermissionsShowPrompt(origin, callback)
}

fun onGeolocationPermissionsHidePrompt() {
internal fun onGeolocationPermissionsHidePrompt() {
return eventProcessor.onGeolocationPermissionsHidePrompt()
}

fun onShowFileChooser(
internal fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams,
): Boolean {
return eventProcessor.onShowFileChooser(webView, filePathCallback, fileChooserParams)
}

fun onPermissionRequest(permissionRequest: PermissionRequest) {
internal fun onPermissionRequest(permissionRequest: PermissionRequest) {
onMainThread {
eventProcessor.onPermissionRequest(permissionRequest)
}
}

fun onCheckoutViewLoadComplete() {
internal fun onCheckoutViewLoadComplete() {
onMainThread {
setProgressBarVisibility(INVISIBLE)
}
}

fun updateProgressBar(progress: Int) {
internal fun updateProgressBar(progress: Int) {
onMainThread {
updateProgressBarPercentage(progress)
}
}

fun onCheckoutViewLoadStarted() {
internal fun onCheckoutViewLoadStarted() {
onMainThread {
setProgressBarVisibility(VISIBLE)
}
}

fun onWebPixelEvent(event: PixelEvent) {
internal fun onWebPixelEvent(event: PixelEvent) {
log.d(LOG_TAG, "Calling onWebPixelEvent for $event.")
eventProcessor.onWebPixelEvent(event)
}

fun onAddressChangeRequested(event: CheckoutAddressChangeRequestedEvent) {
internal fun onAddressChangeRequested(event: CheckoutAddressChangeRequestedEvent) {
onMainThread {
eventProcessor.onAddressChangeRequested(event)
}
}

companion object {
internal companion object {
private const val LOG_TAG = "CheckoutWebViewEventProcessor"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import com.shopify.checkoutsheetkit.lifecycleevents.emptyCompleteEvent
internal class FallbackWebView(context: Context, attributeSet: AttributeSet? = null) :
BaseWebView(context, attributeSet) {

override val recoverErrors = false
internal override val recoverErrors = false
override var checkoutOptions: CheckoutOptions? = null

init {
Expand Down Expand Up @@ -66,7 +66,7 @@ internal class FallbackWebView(context: Context, attributeSet: AttributeSet? = n
return checkoutEventProcessor
}

inner class FallbackWebViewClient : BaseWebView.BaseWebViewClient() {
internal inner class FallbackWebViewClient : BaseWebView.BaseWebViewClient() {

private val typRegex = Regex(pattern = "^(thank[-_]+you)$", option = RegexOption.IGNORE_CASE)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package com.shopify.checkoutsheetkit
import androidx.activity.ComponentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlin.jvm.JvmOverloads

/**
* Entrypoint to the library, allows configuring, preloading, and presenting Shopify checkouts.
Expand Down