Skip to content

Commit 47174da

Browse files
committed
increase flexibility around permissions by delegating to processor
1 parent 2712719 commit 47174da

File tree

10 files changed

+218
-101
lines changed

10 files changed

+218
-101
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
## 3.0.2 Jul 26, 2024
44

5-
- Implements `webChromeClient.onPermissionRequest()` to grant permission to the camera to allow apps that require the camera to verify identity to function correctly.
6-
- Relevant camera permissions must be added to the client's `AndroidManifest.xml`
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.
76

87
## 3.0.1 May 31, 2024
98

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/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ package com.shopify.checkoutsheetkit
2525
import android.Manifest
2626
import android.annotation.SuppressLint
2727
import android.content.Context
28-
import android.content.pm.PackageManager
2928
import android.graphics.Color.TRANSPARENT
3029
import android.os.Build
3130
import android.util.AttributeSet
@@ -71,30 +70,7 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
7170
getEventProcessor().updateProgressBar(newProgress)
7271
}
7372
override fun onPermissionRequest(request: PermissionRequest) {
74-
request.resources?.forEach { resource ->
75-
if (resource == PermissionRequest.RESOURCE_VIDEO_CAPTURE) {
76-
if (!hasCameraPermission()) {
77-
requestPermissions()
78-
request.deny()
79-
} else {
80-
request.grant(request.resources)
81-
}
82-
}
83-
}
84-
}
85-
86-
private fun hasCameraPermission() =
87-
ActivityCompat.checkSelfPermission(
88-
context,
89-
Manifest.permission.CAMERA
90-
) == PackageManager.PERMISSION_GRANTED
91-
92-
private fun requestPermissions() {
93-
ActivityCompat.requestPermissions(
94-
context as ComponentActivity,
95-
arrayOf(Manifest.permission.CAMERA),
96-
CAMERA_PERMISSION_REQUEST
97-
)
73+
getEventProcessor().onPermissionRequest(request)
9874
}
9975
}
10076
isHorizontalScrollBarEnabled = false
@@ -225,7 +201,6 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
225201
companion object {
226202
private const val DEPRECATED_REASON_HEADER = "X-Shopify-API-Deprecated-Reason"
227203
private const val LIQUID_NOT_SUPPORTED = "checkout_liquid_not_supported"
228-
private const val CAMERA_PERMISSION_REQUEST = 1
229204

230205
private const val TOO_MANY_REQUESTS = 429
231206
private val CLIENT_ERROR = 400..499

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/CheckoutWebViewTest.kt

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -205,32 +205,19 @@ class CheckoutWebViewTest {
205205
}
206206

207207
@Test
208-
fun `calls grant when video capture resource permission requested and app has camera permission`() {
209-
val application = shadowOf(activity.application)
210-
application.grantPermissions(Manifest.permission.CAMERA)
211-
208+
fun `calls processors onPermissionRequest when resource permission requested`() {
212209
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)
213-
val permissionRequest = mock<PermissionRequest>()
214-
val requestedResources = arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
215-
whenever(permissionRequest.resources).thenReturn(requestedResources)
216-
217-
val shadow = shadowOf(view)
218-
shadow.webChromeClient?.onPermissionRequest(permissionRequest)
219-
220-
verify(permissionRequest).grant(requestedResources)
221-
}
210+
val webViewEventProcessor = mock<CheckoutWebViewEventProcessor>()
211+
view.setEventProcessor(webViewEventProcessor)
222212

223-
@Test
224-
fun `calls deny when video capture resource permission requested and app does not have camera permission`() {
225-
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)
226213
val permissionRequest = mock<PermissionRequest>()
227214
val requestedResources = arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
228215
whenever(permissionRequest.resources).thenReturn(requestedResources)
229216

230217
val shadow = shadowOf(view)
231218
shadow.webChromeClient?.onPermissionRequest(permissionRequest)
232219

233-
verify(permissionRequest).deny()
220+
verify(webViewEventProcessor).onPermissionRequest(permissionRequest)
234221
}
235222

236223
@Test

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-feature android:name="android.hardware.camera.any" android:required="false"/>
6+
<uses-feature android:name="android.hardware.microphone" android:required="false"/>
7+
68
<uses-permission android:name="android.permission.INTERNET" />
79
<uses-permission android:name="android.permission.CAMERA" />
10+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
811
<application
912
android:name=".MobileBuyIntegration"
1013
android:allowBackup="true"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
context.getActivity()?.let { activity ->
87+
if (Permissions.hasPermission(activity, permissionRequest)) {
88+
permissionRequest.grant(permissionRequest.resources)
89+
} else {
90+
ActivityCompat.requestPermissions(
91+
activity,
92+
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
93+
Permissions.PERMISSION_REQUEST_CODE,
94+
)
95+
permissionRequest.deny()
96+
}
97+
}
98+
}
99+
100+
override fun onWebPixelEvent(event: PixelEvent) {
101+
logger.log(event)
102+
103+
// handle pixel events (e.g. transform, augment, and process), e.g.
104+
val analyticsEvent = when (event) {
105+
is StandardPixelEvent -> event.toAnalyticsEvent()
106+
is CustomPixelEvent -> event.toAnalyticsEvent()
107+
}
108+
109+
analyticsEvent?.let {
110+
Analytics.record(analyticsEvent)
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)