Skip to content

Commit 284957a

Browse files
authored
Prevent destroying presented views if preload is called after present (#104)
prevent destroying presented views if preload is called after present
1 parent db0c30c commit 284957a

File tree

6 files changed

+211
-25
lines changed

6 files changed

+211
-25
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import android.webkit.WebView
4040
import android.webkit.WebViewClient
4141
import java.net.HttpURLConnection.HTTP_GONE
4242
import java.net.HttpURLConnection.HTTP_NOT_FOUND
43-
import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
4443

4544
@SuppressLint("SetJavaScriptEnabled")
4645
internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet? = null) :

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,22 @@ public object ShopifyCheckoutSheetKit {
8282
@JvmStatic
8383
public fun preload(checkoutUrl: String, context: ComponentActivity) {
8484
if (!configuration.preloading.enabled) return
85-
CheckoutWebView.clearCache()
86-
CheckoutWebView.cacheableCheckoutView(
87-
url = checkoutUrl,
88-
activity = context,
89-
isPreload = true,
90-
)
85+
86+
val cacheEntry = CheckoutWebView.cacheEntry
87+
if (cacheEntry?.view != null && cacheEntry.view.isInViewHierarchy()) {
88+
if (cacheEntry.key != checkoutUrl) {
89+
CheckoutWebView.markCacheEntryStale()
90+
}
91+
92+
cacheEntry.view.loadCheckout(checkoutUrl, false)
93+
} else {
94+
CheckoutWebView.markCacheEntryStale()
95+
CheckoutWebView.cacheableCheckoutView(
96+
url = checkoutUrl,
97+
activity = context,
98+
isPreload = true,
99+
)
100+
}
91101
}
92102

93103
/**
@@ -129,4 +139,8 @@ public fun interface CheckoutSheetKitDialog {
129139
* Dismisses the checkout sheet dialog.
130140
*/
131141
public fun dismiss()
132-
}
142+
}
143+
144+
private fun CheckoutWebView.isInViewHierarchy(): Boolean {
145+
return this.parent != null
146+
}

lib/src/test/java/com/shopify/checkoutsheetkit/CheckoutDialogTest.kt

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import android.widget.RelativeLayout
3131
import androidx.activity.ComponentActivity
3232
import androidx.appcompat.widget.Toolbar
3333
import androidx.core.view.children
34-
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
3534
import com.shopify.checkoutsheetkit.lifecycleevents.emptyCompletedEvent
3635
import org.assertj.core.api.Assertions.assertThat
3736
import org.assertj.core.api.Assertions.fail
@@ -67,7 +66,7 @@ class CheckoutDialogTest {
6766
it.preloading = Preloading(enabled = false)
6867
}
6968
activity = Robolectric.buildActivity(ComponentActivity::class.java).get()
70-
processor = defaultCheckoutEventProcessor()
69+
processor = noopDefaultCheckoutEventProcessor(activity)
7170
}
7271

7372
@After
@@ -364,20 +363,6 @@ class CheckoutDialogTest {
364363
return layout.children.any { clazz.isInstance(it) }
365364
}
366365

367-
private fun defaultCheckoutEventProcessor(): DefaultCheckoutEventProcessor {
368-
return object : DefaultCheckoutEventProcessor(activity) {
369-
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
370-
// no-op
371-
}
372-
override fun onCheckoutFailed(error: CheckoutException) {
373-
// no-op
374-
}
375-
override fun onCheckoutCanceled() {
376-
// no-op
377-
}
378-
}
379-
}
380-
381366
private fun checkoutException(isRecoverable: Boolean): CheckoutException {
382367
return CheckoutSheetKitException(
383368
errorCode = CheckoutSheetKitException.ERROR_SENDING_MESSAGE_TO_CHECKOUT,

lib/src/test/java/com/shopify/checkoutsheetkit/CheckoutWebViewClientTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import android.webkit.WebResourceRequest
2929
import android.webkit.WebResourceResponse
3030
import android.webkit.WebViewClient.ERROR_BAD_URL
3131
import androidx.activity.ComponentActivity
32+
import com.shopify.checkoutsheetkit.CheckoutExceptionAssert.Companion.assertThat
3233
import org.assertj.core.api.Assertions.assertThat
3334
import org.junit.Before
3435
import org.junit.Test
@@ -46,7 +47,6 @@ import org.robolectric.annotation.Config
4647
import org.robolectric.shadows.ShadowLooper
4748
import java.net.HttpURLConnection
4849
import kotlin.time.Duration.Companion.minutes
49-
import com.shopify.checkoutsheetkit.CheckoutExceptionAssert.Companion.assertThat
5050

5151
@RunWith(RobolectricTestRunner::class)
5252
class CheckoutWebViewClientTest {

lib/src/test/java/com/shopify/checkoutsheetkit/Helpers.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
*/
2323
package com.shopify.checkoutsheetkit
2424

25+
import android.app.Activity
26+
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
2527
import org.assertj.core.api.AbstractAssert
2628

2729
fun withPreloadingEnabled(block: () -> Unit) {
@@ -96,3 +98,17 @@ class CheckoutExceptionAssert(actual: CheckoutException) :
9698
return this
9799
}
98100
}
101+
102+
fun noopDefaultCheckoutEventProcessor(activity: Activity): DefaultCheckoutEventProcessor {
103+
return object : DefaultCheckoutEventProcessor(activity) {
104+
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
105+
// no-op
106+
}
107+
override fun onCheckoutFailed(error: CheckoutException) {
108+
// no-op
109+
}
110+
override fun onCheckoutCanceled() {
111+
// no-op
112+
}
113+
}
114+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright 2023-present, Shopify Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
package com.shopify.checkoutsheetkit
24+
25+
import androidx.activity.ComponentActivity
26+
import org.assertj.core.api.Assertions.assertThat
27+
import org.junit.After
28+
import org.junit.Before
29+
import org.junit.Test
30+
import org.junit.runner.RunWith
31+
import org.robolectric.Robolectric
32+
import org.robolectric.RobolectricTestRunner
33+
import org.robolectric.Shadows.shadowOf
34+
import org.robolectric.shadows.ShadowLooper
35+
36+
@RunWith(RobolectricTestRunner::class)
37+
class ShopifyCheckoutSheetKitTest {
38+
39+
@Before
40+
fun setUp() {
41+
ShopifyCheckoutSheetKit.configure {
42+
it.preloading = Preloading(enabled = false)
43+
}
44+
}
45+
46+
@After
47+
fun tearDown() {
48+
CheckoutWebView.cacheEntry = null
49+
}
50+
51+
@Test
52+
fun `preload is a noop if preloading is not enabled`() {
53+
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
54+
val url = "https://shopify.dev"
55+
ShopifyCheckoutSheetKit.preload(url, activityController.get())
56+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
57+
58+
assertThat(CheckoutWebView.cacheEntry).isNull()
59+
}
60+
}
61+
62+
@Test
63+
fun `preload caches a WebView and loads the URL if cache is currently empty`() {
64+
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
65+
withPreloadingEnabled {
66+
val url = "https://shopify.dev"
67+
ShopifyCheckoutSheetKit.preload(url, activityController.get())
68+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
69+
70+
assertThat(CheckoutWebView.cacheEntry).isNotNull()
71+
val entry = CheckoutWebView.cacheEntry!!
72+
73+
assertThat(entry.isStale).isFalse()
74+
assertThat(entry.view).isInstanceOf(CheckoutWebView::class.java)
75+
}
76+
}
77+
}
78+
79+
@Test
80+
fun `preload caches a new WebView, loads the URL, and destroys old view if cache is not empty`() {
81+
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
82+
withPreloadingEnabled {
83+
val url = "https://shopify.dev"
84+
ShopifyCheckoutSheetKit.preload(url, activityController.get())
85+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
86+
val originalEntry = CheckoutWebView.cacheEntry
87+
88+
ShopifyCheckoutSheetKit.preload(url, activityController.get())
89+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
90+
91+
assertThat(CheckoutWebView.cacheEntry).isNotNull()
92+
val entry = CheckoutWebView.cacheEntry!!
93+
94+
assertThat(entry.isStale).isFalse()
95+
assertThat(entry.view).isInstanceOf(CheckoutWebView::class.java)
96+
assertThat(entry.view).isNotEqualTo(originalEntry)
97+
assertThat(shadowOf(originalEntry?.view).wasDestroyCalled()).isTrue()
98+
}
99+
}
100+
}
101+
102+
@Test
103+
fun `preload while presented loads url in the existing view`() {
104+
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
105+
withPreloadingEnabled {
106+
val activity = activityController.get()
107+
108+
// first preload caches the view
109+
ShopifyCheckoutSheetKit.preload("https://one.com", activityController.get())
110+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
111+
112+
val originalEntry = CheckoutWebView.cacheEntry
113+
assertThat(originalEntry!!.key).isEqualTo("https://one.com")
114+
assertThat(originalEntry.isStale).isFalse()
115+
116+
// present loads the cached view
117+
ShopifyCheckoutSheetKit.present("https://one.com", activity, noopDefaultCheckoutEventProcessor(activity))
118+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
119+
120+
val secondEntry = CheckoutWebView.cacheEntry
121+
assertThat(secondEntry!!.key).isEqualTo("https://one.com")
122+
assertThat(secondEntry.isStale).isFalse()
123+
124+
// preload after present loads URL in the cached view
125+
ShopifyCheckoutSheetKit.preload("https://one.com", activityController.get())
126+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
127+
128+
val thirdEntry = CheckoutWebView.cacheEntry
129+
assertThat(thirdEntry!!.key).isEqualTo("https://one.com")
130+
assertThat(thirdEntry.isStale).isFalse()
131+
132+
assertThat(shadowOf(thirdEntry.view).lastLoadedUrl).isEqualTo("https://one.com")
133+
}
134+
}
135+
}
136+
137+
@Test
138+
fun `preload after presented loads the url and marks cache stale if url doesn't match cache key`() {
139+
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
140+
withPreloadingEnabled {
141+
val activity = activityController.get()
142+
143+
// first preload caches the view
144+
ShopifyCheckoutSheetKit.preload("https://one.com", activityController.get())
145+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
146+
147+
val originalEntry = CheckoutWebView.cacheEntry
148+
assertThat(originalEntry?.key).isEqualTo("https://one.com")
149+
assertThat(originalEntry?.isStale).isFalse()
150+
151+
// present loads the cached view
152+
ShopifyCheckoutSheetKit.present("https://one.com", activity, noopDefaultCheckoutEventProcessor(activity))
153+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
154+
155+
val secondEntry = CheckoutWebView.cacheEntry
156+
assertThat(secondEntry?.key).isEqualTo("https://one.com")
157+
assertThat(secondEntry?.isStale).isFalse()
158+
159+
// preload after present loads URL in the cached view
160+
ShopifyCheckoutSheetKit.preload("https://two.com", activityController.get())
161+
ShadowLooper.shadowMainLooper().runToEndOfTasks()
162+
163+
// as the URL no longer matches the cache key, mark the cache entry as stale
164+
val thirdEntry = CheckoutWebView.cacheEntry
165+
assertThat(thirdEntry?.key).isEqualTo("https://one.com")
166+
assertThat(thirdEntry?.isStale).isTrue()
167+
168+
assertThat(shadowOf(thirdEntry?.view).lastLoadedUrl).isEqualTo("https://two.com")
169+
}
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)