Skip to content

Commit

Permalink
Add onPermissionRequest to allow clients to camera permissions for ve…
Browse files Browse the repository at this point in the history
…rification apps (#114)
  • Loading branch information
kiftio authored Jul 29, 2024
1 parent 35f6ec5 commit 6cb7598
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 61 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.0.2 Jul 26, 2024

- 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.

## 3.0.1 May 31, 2024

- 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.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
// Called when a web pixel event is emitted in checkout.
// Use this to submit events to your analytics system, see below.
}
override fun onPermissionRequest(permissionRequest: PermissionRequest) {
// Called when a permission has been requested, e.g. to access the camera
// implement to grant/deny/request permissions.
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def resolveEnvVarValue(name, defaultValue) {
return rawValue ? rawValue : defaultValue
}

def versionName = resolveEnvVarValue("CHECKOUT_SHEET_KIT_VERSION", "3.0.1")
def versionName = resolveEnvVarValue("CHECKOUT_SHEET_KIT_VERSION", "3.0.2")

ext {
app_compat_version = '1.6.1'
Expand Down
4 changes: 4 additions & 0 deletions lib/src/main/java/com/shopify/checkoutsheetkit/BaseWebView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.webkit.PermissionRequest
import android.webkit.RenderProcessGoneDetail
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
Expand Down Expand Up @@ -65,6 +66,9 @@ internal abstract class BaseWebView(context: Context, attributeSet: AttributeSet
super.onProgressChanged(view, newProgress)
getEventProcessor().updateProgressBar(newProgress)
}
override fun onPermissionRequest(request: PermissionRequest) {
getEventProcessor().onPermissionRequest(request)
}
}
isHorizontalScrollBarEnabled = false
requestDisallowInterceptTouchEvent(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package com.shopify.checkoutsheetkit
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.PermissionRequest
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent

Expand Down Expand Up @@ -58,6 +59,11 @@ public interface CheckoutEventProcessor {
*/
public fun onCheckoutLinkClicked(uri: Uri)

/**
* A permission has been requested by the web chrome client, e.g. to access the camera
*/
public fun onPermissionRequest(permissionRequest: PermissionRequest)

/**
* Web Pixel event emitted from checkout, that can be optionally transformed, enhanced (e.g. with user and session identifiers),
* and processed
Expand All @@ -80,6 +86,9 @@ internal class NoopEventProcessor : CheckoutEventProcessor {

override fun onWebPixelEvent(event: PixelEvent) {/* noop */
}

override fun onPermissionRequest(permissionRequest: PermissionRequest) {/* noop */
}
}

/**
Expand All @@ -105,6 +114,10 @@ public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor(
// no-op, override to implement
}

override fun onPermissionRequest(permissionRequest: PermissionRequest) {
// no-op override to implement
}

private fun Context.launchEmailApp(to: String) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "vnd.android.cursor.item/email"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import android.os.Handler
import android.os.Looper
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.webkit.PermissionRequest
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent

Expand Down Expand Up @@ -62,6 +63,12 @@ internal class CheckoutWebViewEventProcessor(
}
}

fun onPermissionRequest(permissionRequest: PermissionRequest) {
onMainThread {
eventProcessor.onPermissionRequest(permissionRequest)
}
}

fun onCheckoutViewLoadComplete() {
onMainThread {
setProgressBarVisibility(INVISIBLE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowDialog
import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowWebView
import java.util.concurrent.TimeUnit

@RunWith(RobolectricTestRunner::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import android.graphics.Color
import android.os.Looper
import android.view.View.VISIBLE
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.webkit.PermissionRequest
import androidx.activity.ComponentActivity
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
Expand All @@ -37,6 +38,7 @@ import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.kotlin.whenever
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
Expand Down Expand Up @@ -81,7 +83,7 @@ class CheckoutWebViewTest {
ShopifyCheckoutSheetKit.configuration.colorScheme = ColorScheme.Dark()
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)

assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.1 ")
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.2 ")
}

@Test
Expand Down Expand Up @@ -201,6 +203,22 @@ class CheckoutWebViewTest {
verify(webViewEventProcessor).updateProgressBar(50)
}

@Test
fun `calls processors onPermissionRequest when resource permission requested`() {
val view = CheckoutWebView.cacheableCheckoutView(URL, activity)
val webViewEventProcessor = mock<CheckoutWebViewEventProcessor>()
view.setEventProcessor(webViewEventProcessor)

val permissionRequest = mock<PermissionRequest>()
val requestedResources = arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
whenever(permissionRequest.resources).thenReturn(requestedResources)

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

verify(webViewEventProcessor).onPermissionRequest(permissionRequest)
}

@Test
fun `should recover from errors`() {
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class FallbackWebViewTest {
ShopifyCheckoutSheetKit.configuration.colorScheme = ColorScheme.Dark()
Robolectric.buildActivity(ComponentActivity::class.java).use { activityController ->
val view = FallbackWebView(activityController.get())
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.1 ")
assertThat(view.settings.userAgentString).contains("ShopifyCheckoutSDK/3.0.2 ")
}
}

Expand Down
2 changes: 1 addition & 1 deletion samples/MobileBuyIntegration/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ android {
applicationId "com.shopify.checkout_sdk_mobile_buy_integration_sample"
minSdk 23
targetSdk 34
versionCode 31
versionCode 32
versionName "0.0.${versionCode}"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
5 changes: 5 additions & 0 deletions samples/MobileBuyIntegration/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-feature android:name="android.hardware.camera.any" android:required="false"/>
<uses-feature android:name="android.hardware.microphone" android:required="false"/>

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:name=".MobileBuyIntegration"
android:allowBackup="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* MIT License
*
* Copyright 2023-present, Shopify Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.shopify.checkout_sdk_mobile_buy_integration_sample.common

import android.Manifest
import android.content.Context
import android.webkit.PermissionRequest
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.navigation.NavController
import com.shopify.checkout_sdk_mobile_buy_integration_sample.R
import com.shopify.checkout_sdk_mobile_buy_integration_sample.cart.CartViewModel
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.analytics.Analytics
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.analytics.toAnalyticsEvent
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.logs.Logger
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.navigation.Screen
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.navigation.getActivity
import com.shopify.checkout_sdk_mobile_buy_integration_sample.common.permissions.Permissions
import com.shopify.checkoutsheetkit.CheckoutException
import com.shopify.checkoutsheetkit.DefaultCheckoutEventProcessor
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
import com.shopify.checkoutsheetkit.pixelevents.CustomPixelEvent
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
import com.shopify.checkoutsheetkit.pixelevents.StandardPixelEvent
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

@OptIn(DelicateCoroutinesApi::class)
class MobileBuyEventProcessor(
private val cartViewModel: CartViewModel,
private val navController: NavController,
private val logger: Logger,
private val context: Context
): DefaultCheckoutEventProcessor(context) {
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {
logger.log(checkoutCompletedEvent)

cartViewModel.clearCart()
GlobalScope.launch(Dispatchers.Main) {
navController.popBackStack(Screen.Product.route, false)
}
}

override fun onCheckoutFailed(error: CheckoutException) {
logger.log("Checkout failed", error)

if (!error.isRecoverable) {
GlobalScope.launch(Dispatchers.Main) {
Toast.makeText(
context,
context.getText(R.string.checkout_error),
Toast.LENGTH_SHORT
).show()
}
}
}

override fun onCheckoutCanceled() {
// optionally respond to checkout being canceled/closed
logger.log("Checkout canceled")
}

override fun onPermissionRequest(permissionRequest: PermissionRequest) {
logger.log("Permission requested for ${permissionRequest.resources}")
context.getActivity()?.let { activity ->
if (Permissions.hasPermission(activity, permissionRequest)) {
permissionRequest.grant(permissionRequest.resources)
} else {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
Permissions.PERMISSION_REQUEST_CODE,
)
permissionRequest.deny()
}
}
}

override fun onWebPixelEvent(event: PixelEvent) {
logger.log(event)

// handle pixel events (e.g. transform, augment, and process), e.g.
val analyticsEvent = when (event) {
is StandardPixelEvent -> event.toAnalyticsEvent()
is CustomPixelEvent -> event.toAnalyticsEvent()
}

analyticsEvent?.let {
Analytics.record(analyticsEvent)
}
}
}
Loading

0 comments on commit 6cb7598

Please sign in to comment.