Skip to content

Commit 892f69f

Browse files
Prototype development/android inline component (#397)
* use more specific react-native platform value * Accept an application authentication token (#384) Accept app authentication token * feat: setup android inline component for react-native * fix: add event id to CheckoutAddressChangeRequested * fix: warning on visibility of CheckoutWebViewClient * chore: remove unused private keys * fix: visibility for tests * revert lib/gradle changes * revert jvmOverloads import * refactor: removing redundant modifiers * fix: visibility modifiersa nd add todo --------- Co-authored-by: Daniel Kift <daniel.kift@shopify.com>
1 parent 44741f7 commit 892f69f

File tree

6 files changed

+112
-41
lines changed

6 files changed

+112
-41
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -664,21 +664,24 @@ see [guidelines and instructions](.github/CONTRIBUTING.md).
664664
665665
Shopify's Checkout Kit is provided under an [MIT License](LICENSE).
666666
667-
668-
----
667+
---
669668
670669
## Migration
671670
672-
### Currently Lost
671+
### Currently Lost
672+
673673
#### Outbound
674+
674675
- presented
675676
- instrumentation (deprecated)
676677
677678
#### Inbound
679+
678680
- pixels
679681
- blocking
680682
- error
681683
682684
#### Bugs and stuff
685+
683686
- evaling js... relying on cache - not safe
684687
- not sending onError yet

lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit.log
4949
import java.net.HttpURLConnection.HTTP_GONE
5050

5151
@SuppressLint("SetJavaScriptEnabled")
52-
internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet? = null) :
52+
public abstract class BaseWebView(context: Context, attributeSet: AttributeSet? = null) :
5353
WebView(context, attributeSet) {
5454

5555
internal val authenticationTracker = AuthenticationTracker()
@@ -58,10 +58,10 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
5858
configureWebView()
5959
}
6060

61-
abstract fun getEventProcessor(): CheckoutWebViewEventProcessor
62-
abstract val recoverErrors: Boolean
61+
internal abstract fun getEventProcessor(): CheckoutWebViewEventProcessor
62+
internal abstract val recoverErrors: Boolean
6363

64-
open val checkoutOptions: CheckoutOptions? = null
64+
protected open val checkoutOptions: CheckoutOptions? = null
6565

6666
private fun configureWebView() {
6767
visibility = VISIBLE
@@ -123,7 +123,7 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
123123
return super.onKeyDown(keyCode, event)
124124
}
125125

126-
open inner class BaseWebViewClient : WebViewClient() {
126+
internal open inner class BaseWebViewClient : WebViewClient() {
127127
init {
128128
if (BuildConfig.DEBUG) {
129129
log.d(LOG_TAG, "Setting web contents debugging enabled.")
@@ -246,8 +246,9 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
246246
}
247247
}
248248

249-
companion object {
249+
internal companion object {
250250
private const val LOG_TAG = "BaseWebView"
251+
251252
private const val TOO_MANY_REQUESTS = 429
252253
private val CLIENT_ERROR = 400..499
253254
}

lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutAddressChangeRequestedEvent.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public data class CheckoutAddressChangeRequestedEventData(
4242
public class CheckoutAddressChangeRequestedEvent internal constructor(
4343
internal val message: CheckoutMessageParser.JSONRPCMessage.AddressChangeRequested,
4444
) {
45+
public val id: String? get() = message.id
4546
public val addressType: String get() = message.addressType
4647
public val selectedAddress: CartDeliveryAddressInput? get() = message.selectedAddress
4748

lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutBridge.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import android.webkit.JavascriptInterface
2828
import android.webkit.WebView
2929
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit.log
3030
import kotlinx.serialization.Serializable
31+
import kotlinx.serialization.decodeFromString
3132
import kotlinx.serialization.json.Json
3233
import java.lang.ref.WeakReference
3334

@@ -38,6 +39,16 @@ internal class CheckoutBridge(
3839

3940
private var webViewRef: WeakReference<WebView>? = null
4041

42+
/**
43+
* TODO:
44+
* This is architecturally the inverse of what we do for iOS, where the RCTCheckoutWebView.swift holds the list of events active
45+
* Once the AddressPicker screen is in, as the Android native apps can pass references to the events directly
46+
* We can move this to the RCTCheckoutWebView.java, and remove the webViewRef as that exists on
47+
* the events.
48+
* This doesn't affect behaviour just means they're consistent
49+
*/
50+
private val pendingEvents = mutableMapOf<String, CheckoutMessageParser.JSONRPCMessage>()
51+
4152
fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
4253
this.eventProcessor = eventProcessor
4354
}
@@ -48,6 +59,29 @@ internal class CheckoutBridge(
4859
this.webViewRef = if (webView != null) WeakReference(webView) else null
4960
}
5061

62+
/**
63+
* Respond to an RPC event with the given response data
64+
* @param eventId The ID of the event to respond to
65+
* @param responseData The JSON response data to send back
66+
*/
67+
fun respondToEvent(eventId: String, responseData: String) {
68+
val event = pendingEvents[eventId]
69+
if (event is CheckoutMessageParser.JSONRPCMessage.AddressChangeRequested) {
70+
try {
71+
// Parse the response data as DeliveryAddressChangePayload
72+
val jsonParser = Json { ignoreUnknownKeys = true }
73+
val payload = jsonParser.decodeFromString<DeliveryAddressChangePayload>(responseData)
74+
event.respondWith(payload)
75+
pendingEvents.remove(eventId)
76+
log.d(LOG_TAG, "Successfully responded to event $eventId")
77+
} catch (e: Exception) {
78+
log.e(LOG_TAG, "Failed to parse response data for event $eventId: ${e.message}")
79+
}
80+
} else {
81+
log.w(LOG_TAG, "No pending event found with ID $eventId")
82+
}
83+
}
84+
5185
// Allows Web to postMessages back to the SDK
5286
@Suppress("SwallowedException")
5387
@JavascriptInterface
@@ -61,6 +95,10 @@ internal class CheckoutBridge(
6195
webViewRef?.get()?.let { webView ->
6296
checkoutMessage.setWebView(webView)
6397
}
98+
// Store the event for potential React Native response
99+
checkoutMessage.id?.let { id ->
100+
pendingEvents[id] = checkoutMessage
101+
}
64102
onMainThread {
65103
eventProcessor.onAddressChangeRequested(checkoutMessage.toEvent())
66104
}

lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebView.kt

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ import java.util.concurrent.CountDownLatch
3737
import kotlin.math.abs
3838
import kotlin.time.Duration.Companion.minutes
3939

40-
internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = null) :
40+
public class CheckoutWebView(context: Context, attributeSet: AttributeSet? = null) :
4141
BaseWebView(context, attributeSet) {
4242

43-
override val recoverErrors = true
44-
var isPreload = false
43+
override val recoverErrors: Boolean = true
44+
private var isPreload: Boolean = false
4545
override var checkoutOptions: CheckoutOptions? = null
4646

4747
private val checkoutBridge = CheckoutBridge(CheckoutWebViewEventProcessor(NoopEventProcessor()))
@@ -62,18 +62,37 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
6262
checkoutBridge.setWebView(this)
6363
}
6464

65-
fun hasFinishedLoading() = loadComplete
65+
public fun hasFinishedLoading(): Boolean = loadComplete
6666

67-
fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
67+
public fun setEventProcessor(eventProcessor: CheckoutWebViewEventProcessor) {
6868
log.d(LOG_TAG, "Setting event processor $eventProcessor.")
6969
checkoutBridge.setEventProcessor(eventProcessor)
7070
}
7171

72-
fun notifyPresented() {
72+
public fun notifyPresented() {
7373
log.d(LOG_TAG, "Notify presented called.")
7474
presented = true
7575
}
7676

77+
/**
78+
* Reload the current checkout page
79+
*/
80+
public override fun reload() {
81+
log.d(LOG_TAG, "Reloading checkout")
82+
super.reload()
83+
}
84+
85+
/**
86+
* Respond to an RPC event (e.g. address change, payment change)
87+
* @param eventId The ID of the event to respond to
88+
* @param responseData The response data to send back
89+
*/
90+
public fun respondToEvent(eventId: String, responseData: String) {
91+
log.d(LOG_TAG, "Responding to event $eventId with data: $responseData")
92+
// Delegate to the CheckoutBridge to handle the response
93+
checkoutBridge.respondToEvent(eventId, responseData)
94+
}
95+
7796
override fun getEventProcessor(): CheckoutWebViewEventProcessor {
7897
return checkoutBridge.getEventProcessor()
7998
}
@@ -92,7 +111,7 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
92111
checkoutBridge.setWebView(null)
93112
}
94113

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

113-
inner class CheckoutWebViewClient : BaseWebView.BaseWebViewClient() {
132+
internal inner class CheckoutWebViewClient : BaseWebView.BaseWebViewClient() {
114133

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

176-
companion object {
195+
public companion object {
177196
private const val LOG_TAG = "CheckoutWebView"
178197
private const val OPEN_EXTERNALLY_PARAM = "open_externally"
179198
private const val JAVASCRIPT_INTERFACE_NAME = "EmbeddedCheckoutProtocolConsumer"
180199

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

184-
fun markCacheEntryStale() {
203+
public fun markCacheEntryStale() {
185204
cacheEntry = cacheEntry?.copy(timeout = -1)
186205
}
187206

188-
fun clearCache(newEntry: CheckoutWebViewCacheEntry? = null) = cacheEntry?.let {
189-
Handler(Looper.getMainLooper()).post {
190-
it.view.destroy()
191-
cacheEntry = newEntry
207+
/**
208+
* Clear the cached CheckoutWebView
209+
*/
210+
public fun clearCache() {
211+
clearCacheInternal(null)
212+
}
213+
214+
internal fun clearCacheInternal(newEntry: CheckoutWebViewCacheEntry? = null) {
215+
cacheEntry?.let {
216+
Handler(Looper.getMainLooper()).post {
217+
it.view.destroy()
218+
cacheEntry = newEntry
219+
}
192220
}
193221
}
194222

195-
fun cacheableCheckoutView(
223+
internal fun cacheableCheckoutView(
196224
url: String,
197225
activity: ComponentActivity,
198226
isPreload: Boolean = false,
@@ -256,7 +284,7 @@ internal class CheckoutWebView(context: Context, attributeSet: AttributeSet? = n
256284
this.cacheEntry = cacheEntry
257285
} else {
258286
log.d(LOG_TAG, "Clearing WebView cache and destroying cached view.")
259-
clearCache(cacheEntry)
287+
clearCacheInternal(cacheEntry)
260288
}
261289
}
262290
}

lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutWebViewEventProcessor.kt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,90 +38,90 @@ import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
3838
* Event processor that can handle events internally, delegate to the CheckoutEventProcessor
3939
* passed into ShopifyCheckoutSheetKit.present(), or preprocess arguments and then delegate
4040
*/
41-
internal class CheckoutWebViewEventProcessor(
41+
public class CheckoutWebViewEventProcessor(
4242
private val eventProcessor: CheckoutEventProcessor,
4343
private val toggleHeader: (Boolean) -> Unit = {},
44-
private val closeCheckoutDialogWithError: (CheckoutException) -> Unit = { CheckoutWebView.clearCache() },
44+
private val closeCheckoutDialogWithError: (CheckoutException) -> Unit = { CheckoutWebView.clearCacheInternal() },
4545
private val setProgressBarVisibility: (Int) -> Unit = {},
4646
private val updateProgressBarPercentage: (Int) -> Unit = {},
4747
) {
48-
fun onCheckoutViewComplete(checkoutCompleteEvent: CheckoutCompleteEvent) {
48+
internal fun onCheckoutViewComplete(checkoutCompleteEvent: CheckoutCompleteEvent) {
4949
log.d(LOG_TAG, "Clearing WebView cache after checkout completion.")
5050
CheckoutWebView.markCacheEntryStale()
5151

5252
log.d(LOG_TAG, "Calling onCheckoutCompleted $checkoutCompleteEvent.")
5353
eventProcessor.onCheckoutCompleted(checkoutCompleteEvent)
5454
}
5555

56-
fun onCheckoutViewModalToggled(modalVisible: Boolean) {
56+
internal fun onCheckoutViewModalToggled(modalVisible: Boolean) {
5757
onMainThread {
5858
toggleHeader(modalVisible)
5959
}
6060
}
6161

62-
fun onCheckoutViewLinkClicked(uri: Uri) {
62+
internal fun onCheckoutViewLinkClicked(uri: Uri) {
6363
log.d(LOG_TAG, "Calling onCheckoutLinkClicked.")
6464
eventProcessor.onCheckoutLinkClicked(uri)
6565
}
6666

67-
fun onCheckoutViewFailedWithError(error: CheckoutException) {
67+
internal fun onCheckoutViewFailedWithError(error: CheckoutException) {
6868
onMainThread {
6969
closeCheckoutDialogWithError(error)
7070
}
7171
}
7272

73-
fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
73+
internal fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
7474
return eventProcessor.onGeolocationPermissionsShowPrompt(origin, callback)
7575
}
7676

77-
fun onGeolocationPermissionsHidePrompt() {
77+
internal fun onGeolocationPermissionsHidePrompt() {
7878
return eventProcessor.onGeolocationPermissionsHidePrompt()
7979
}
8080

81-
fun onShowFileChooser(
81+
internal fun onShowFileChooser(
8282
webView: WebView,
8383
filePathCallback: ValueCallback<Array<Uri>>,
8484
fileChooserParams: FileChooserParams,
8585
): Boolean {
8686
return eventProcessor.onShowFileChooser(webView, filePathCallback, fileChooserParams)
8787
}
8888

89-
fun onPermissionRequest(permissionRequest: PermissionRequest) {
89+
internal fun onPermissionRequest(permissionRequest: PermissionRequest) {
9090
onMainThread {
9191
eventProcessor.onPermissionRequest(permissionRequest)
9292
}
9393
}
9494

95-
fun onCheckoutViewLoadComplete() {
95+
internal fun onCheckoutViewLoadComplete() {
9696
onMainThread {
9797
setProgressBarVisibility(INVISIBLE)
9898
}
9999
}
100100

101-
fun updateProgressBar(progress: Int) {
101+
internal fun updateProgressBar(progress: Int) {
102102
onMainThread {
103103
updateProgressBarPercentage(progress)
104104
}
105105
}
106106

107-
fun onCheckoutViewLoadStarted() {
107+
internal fun onCheckoutViewLoadStarted() {
108108
onMainThread {
109109
setProgressBarVisibility(VISIBLE)
110110
}
111111
}
112112

113-
fun onWebPixelEvent(event: PixelEvent) {
113+
internal fun onWebPixelEvent(event: PixelEvent) {
114114
log.d(LOG_TAG, "Calling onWebPixelEvent for $event.")
115115
eventProcessor.onWebPixelEvent(event)
116116
}
117117

118-
fun onAddressChangeRequested(event: CheckoutAddressChangeRequestedEvent) {
118+
internal fun onAddressChangeRequested(event: CheckoutAddressChangeRequestedEvent) {
119119
onMainThread {
120120
eventProcessor.onAddressChangeRequested(event)
121121
}
122122
}
123123

124-
companion object {
124+
internal companion object {
125125
private const val LOG_TAG = "CheckoutWebViewEventProcessor"
126126
}
127127
}

0 commit comments

Comments
 (0)