diff --git a/README.md b/README.md index f484f461..65374491 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt b/lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt index 7c2175fe..9c8f32c1 100644 --- a/lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt +++ b/lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt @@ -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() @@ -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 @@ -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.") @@ -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 } diff --git a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutAddressChangeRequestedEvent.kt b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutAddressChangeRequestedEvent.kt index ff83fe1c..9718c681 100644 --- a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutAddressChangeRequestedEvent.kt +++ b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutAddressChangeRequestedEvent.kt @@ -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 diff --git a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutBridge.kt b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutBridge.kt index 4c2ea9eb..829b494b 100644 --- a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutBridge.kt +++ b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutBridge.kt @@ -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 @@ -38,6 +39,16 @@ internal class CheckoutBridge( private var webViewRef: WeakReference? = 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() + fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) { this.eventProcessor = eventProcessor } @@ -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(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 @@ -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()) } diff --git a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt index b3934366..73d570ec 100644 --- a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt +++ b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt @@ -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())) @@ -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() } @@ -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 @@ -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) @@ -173,7 +192,7 @@ 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" @@ -181,18 +200,27 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n 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, @@ -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) } } } diff --git a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebViewEventProcessor.kt b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebViewEventProcessor.kt index 2feb2376..26a67fe2 100644 --- a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebViewEventProcessor.kt +++ b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebViewEventProcessor.kt @@ -38,14 +38,14 @@ 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() @@ -53,32 +53,32 @@ internal class CheckoutWebViewEventProcessor( 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>, fileChooserParams: FileChooserParams, @@ -86,42 +86,42 @@ internal class CheckoutWebViewEventProcessor( 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" } }