Skip to content

Commit 6cb7598

Browse files
authored
Add onPermissionRequest to allow clients to camera permissions for verification apps (#114)
1 parent 35f6ec5 commit 6cb7598

File tree

14 files changed

+245
-61
lines changed

14 files changed

+245
-61
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 3.0.2 Jul 26, 2024
4+
5+
- Implements `onPermissionRequest()` to call a new `eventProcessor.onPermissionRequest(permissionRequest: PermissionRequest)` callback allowing clients to grant or deny permission requests, or request permissions (e.g. camera, record audio). This is sometimes required for checkouts that use features that require verifying identity.
6+
37
## 3.0.1 May 31, 2024
48

59
- Call `onPause()` on the WebView as it's created if preloading, and `onResume()` when it's presented, so the Page Visibility API reports correct values.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
251251
// Called when a web pixel event is emitted in checkout.
252252
// Use this to submit events to your analytics system, see below.
253253
}
254+
255+
override fun onPermissionRequest(permissionRequest: PermissionRequest) {
256+
// Called when a permission has been requested, e.g. to access the camera
257+
// implement to grant/deny/request permissions.
258+
}
254259
}
255260
```
256261

lib/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def resolveEnvVarValue(name, defaultValue) {
1414
return rawValue ? rawValue : defaultValue
1515
}
1616

17-
def versionName = resolveEnvVarValue("CHECKOUT_SHEET_KIT_VERSION", "3.0.1")
17+
def versionName = resolveEnvVarValue("CHECKOUT_SHEET_KIT_VERSION", "3.0.2")
1818

1919
ext {
2020
app_compat_version = '1.6.1'

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import android.view.KeyEvent
3131
import android.view.View
3232
import android.view.ViewGroup.LayoutParams
3333
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
34+
import android.webkit.PermissionRequest
3435
import android.webkit.RenderProcessGoneDetail
3536
import android.webkit.WebChromeClient
3637
import android.webkit.WebResourceError
@@ -65,6 +66,9 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
6566
super.onProgressChanged(view, newProgress)
6667
getEventProcessor().updateProgressBar(newProgress)
6768
}
69+
override fun onPermissionRequest(request: PermissionRequest) {
70+
getEventProcessor().onPermissionRequest(request)
71+
}
6872
}
6973
isHorizontalScrollBarEnabled = false
7074
requestDisallowInterceptTouchEvent(true)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ package com.shopify.checkoutsheetkit
2525
import android.content.Context
2626
import android.content.Intent
2727
import android.net.Uri
28+
import android.webkit.PermissionRequest
2829
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
2930
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
3031

@@ -58,6 +59,11 @@ public interface CheckoutEventProcessor {
5859
*/
5960
public fun onCheckoutLinkClicked(uri: Uri)
6061

62+
/**
63+
* A permission has been requested by the web chrome client, e.g. to access the camera
64+
*/
65+
public fun onPermissionRequest(permissionRequest: PermissionRequest)
66+
6167
/**
6268
* Web Pixel event emitted from checkout, that can be optionally transformed, enhanced (e.g. with user and session identifiers),
6369
* and processed
@@ -80,6 +86,9 @@ internal class NoopEventProcessor : CheckoutEventProcessor {
8086

8187
override fun onWebPixelEvent(event: PixelEvent) {/* noop */
8288
}
89+
90+
override fun onPermissionRequest(permissionRequest: PermissionRequest) {/* noop */
91+
}
8392
}
8493

8594
/**
@@ -105,6 +114,10 @@ public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor(
105114
// no-op, override to implement
106115
}
107116

117+
override fun onPermissionRequest(permissionRequest: PermissionRequest) {
118+
// no-op override to implement
119+
}
120+
108121
private fun Context.launchEmailApp(to: String) {
109122
val intent = Intent(Intent.ACTION_SEND)
110123
intent.type = "vnd.android.cursor.item/email"

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import android.os.Handler
2727
import android.os.Looper
2828
import android.view.View.INVISIBLE
2929
import android.view.View.VISIBLE
30+
import android.webkit.PermissionRequest
3031
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
3132
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
3233

@@ -62,6 +63,12 @@ internal class CheckoutWebViewEventProcessor(
6263
}
6364
}
6465

66+
fun onPermissionRequest(permissionRequest: PermissionRequest) {
67+
onMainThread {
68+
eventProcessor.onPermissionRequest(permissionRequest)
69+
}
70+
}
71+
6572
fun onCheckoutViewLoadComplete() {
6673
onMainThread {
6774
setProgressBarVisibility(INVISIBLE)

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import org.robolectric.RobolectricTestRunner
4949
import org.robolectric.Shadows.shadowOf
5050
import org.robolectric.shadows.ShadowDialog
5151
import org.robolectric.shadows.ShadowLooper
52-
import org.robolectric.shadows.ShadowWebView
5352
import java.util.concurrent.TimeUnit
5453

5554
@RunWith(RobolectricTestRunner::class)

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import android.graphics.Color
2626
import android.os.Looper
2727
import android.view.View.VISIBLE
2828
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
29+
import android.webkit.PermissionRequest
2930
import androidx.activity.ComponentActivity
3031
import org.assertj.core.api.Assertions.assertThat
3132
import org.junit.After
@@ -37,6 +38,7 @@ import org.mockito.ArgumentMatchers.eq
3738
import org.mockito.Mockito.mock
3839
import org.mockito.Mockito.spy
3940
import org.mockito.Mockito.verify
41+
import org.mockito.kotlin.whenever
4042
import org.robolectric.Robolectric
4143
import org.robolectric.RobolectricTestRunner
4244
import org.robolectric.Shadows.shadowOf
@@ -81,7 +83,7 @@ class CheckoutWebViewTest {
8183
ShopifyCheckoutSheetKit.configuration.colorScheme = ColorScheme.Dark()
8284
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)
8385

84-
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.1 ")
86+
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.2 ")
8587
}
8688

8789
@Test
@@ -201,6 +203,22 @@ class CheckoutWebViewTest {
201203
verify(webViewEventProcessor).updateProgressBar(50)
202204
}
203205

206+
@Test
207+
fun `calls processors onPermissionRequest when resource permission requested`() {
208+
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)
209+
val webViewEventProcessor = mock<CheckoutWebViewEventProcessor>()
210+
view.setEventProcessor(webViewEventProcessor)
211+
212+
val permissionRequest = mock<PermissionRequest>()
213+
val requestedResources = arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
214+
whenever(permissionRequest.resources).thenReturn(requestedResources)
215+
216+
val shadow = shadowOf(view)
217+
shadow.webChromeClient?.onPermissionRequest(permissionRequest)
218+
219+
verify(webViewEventProcessor).onPermissionRequest(permissionRequest)
220+
}
221+
204222
@Test
205223
fun `should recover from errors`() {
206224
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class FallbackWebViewTest {
6666
ShopifyCheckoutSheetKit.configuration.colorScheme = ColorScheme.Dark()
6767
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
6868
val view = FallbackWebView(activityController.get())
69-
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.1 ")
69+
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.2 ")
7070
}
7171
}
7272

samples/MobileBuyIntegration/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ android {
3131
applicationId "com.shopify.checkout_sdk_mobile_buy_integration_sample"
3232
minSdk 23
3333
targetSdk 34
34-
versionCode 31
34+
versionCode 32
3535
versionName "0.0.${versionCode}"
3636

3737
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

samples/MobileBuyIntegration/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
44

5+
<uses-feature android:name="android.hardware.camera.any" android:required="false"/>
6+
<uses-feature android:name="android.hardware.microphone" android:required="false"/>
7+
58
<uses-permission android:name="android.permission.INTERNET" />
9+
<uses-permission android:name="android.permission.CAMERA" />
10+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
611
<application
712
android:name=".MobileBuyIntegration"
813
android:allowBackup="true"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.checkout_sdk_mobile_buy_integration_sample.common
24+
25+
import android.Manifest
26+
import android.content.Context
27+
import android.webkit.PermissionRequest
28+
import android.widget.Toast
29+
import androidx.core.app.ActivityCompat
30+
import androidx.navigation.NavController
31+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.R
32+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.cart.CartViewModel
33+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.analytics.Analytics
34+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.analytics.toAnalyticsEvent
35+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.logs.Logger
36+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.navigation.Screen
37+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.navigation.getActivity
38+
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.permissions.Permissions
39+
import com.shopify.checkoutsheetkit.CheckoutException
40+
import com.shopify.checkoutsheetkit.DefaultCheckoutEventProcessor
41+
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
42+
import com.shopify.checkoutsheetkit.pixelevents.CustomPixelEvent
43+
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
44+
import com.shopify.checkoutsheetkit.pixelevents.StandardPixelEvent
45+
import kotlinx.coroutines.DelicateCoroutinesApi
46+
import kotlinx.coroutines.Dispatchers
47+
import kotlinx.coroutines.GlobalScope
48+
import kotlinx.coroutines.launch
49+
50+
@OptIn(DelicateCoroutinesApi::class)
51+
class MobileBuyEventProcessor(
52+
private val cartViewModel: CartViewModel,
53+
private val navController: NavController,
54+
private val logger: Logger,
55+
private val context: Context
56+
): DefaultCheckoutEventProcessor(context) {
57+
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
58+
logger.log(checkoutCompletedEvent)
59+
60+
cartViewModel.clearCart()
61+
GlobalScope.launch(Dispatchers.Main) {
62+
navController.popBackStack(Screen.Product.route, false)
63+
}
64+
}
65+
66+
override fun onCheckoutFailed(error: CheckoutException) {
67+
logger.log("Checkout failed", error)
68+
69+
if (!error.isRecoverable) {
70+
GlobalScope.launch(Dispatchers.Main) {
71+
Toast.makeText(
72+
context,
73+
context.getText(R.string.checkout_error),
74+
Toast.LENGTH_SHORT
75+
).show()
76+
}
77+
}
78+
}
79+
80+
override fun onCheckoutCanceled() {
81+
// optionally respond to checkout being canceled/closed
82+
logger.log("Checkout canceled")
83+
}
84+
85+
override fun onPermissionRequest(permissionRequest: PermissionRequest) {
86+
logger.log("Permission requested for ${permissionRequest.resources}")
87+
context.getActivity()?.let { activity ->
88+
if (Permissions.hasPermission(activity, permissionRequest)) {
89+
permissionRequest.grant(permissionRequest.resources)
90+
} else {
91+
ActivityCompat.requestPermissions(
92+
activity,
93+
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
94+
Permissions.PERMISSION_REQUEST_CODE,
95+
)
96+
permissionRequest.deny()
97+
}
98+
}
99+
}
100+
101+
override fun onWebPixelEvent(event: PixelEvent) {
102+
logger.log(event)
103+
104+
// handle pixel events (e.g. transform, augment, and process), e.g.
105+
val analyticsEvent = when (event) {
106+
is StandardPixelEvent -> event.toAnalyticsEvent()
107+
is CustomPixelEvent -> event.toAnalyticsEvent()
108+
}
109+
110+
analyticsEvent?.let {
111+
Analytics.record(analyticsEvent)
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)