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
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 @@ -38,6 +39,16 @@ internal class CheckoutBridge(

private var webViewRef: WeakReference<WebView>? = null

/**
* TODO:
* This is architecturally the inverse of what we do for iOS, where the RCTCheckoutWebView.swift holds the list of events active
* Once the AddressPicker screen is in, as the Android native apps can pass references to the events directly
* We can move this to the RCTCheckoutWebView.java, and remove the webViewRef as that exists on
* the events.
* This doesn't affect behaviour just means they're consistent
*/
private val pendingEvents = mutableMapOf<String, CheckoutMessageParser.JSONRPCMessage>()

fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
this.eventProcessor = eventProcessor
}
Expand All @@ -48,6 +59,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 +95,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
60 changes: 44 additions & 16 deletions lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ 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 val recoverErrors: Boolean = true
private var isPreload: Boolean = false
override var checkoutOptions: CheckoutOptions? = null

private val checkoutBridge = CheckoutBridge(CheckoutWebViewEventProcessor(NoopEventProcessor()))
Expand All @@ -62,18 +62,37 @@ 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
}

/**
* 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)
}

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(
internal 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"
}
}