diff --git a/README.md b/README.md index 6a72b032..230bc8e2 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ val processor = object : DefaultCheckoutEventProcessor(activity) { ``` > [!Note] -> The `DefaultCheckoutEventProcessor` provides default implementations for current and future callback functions (such as `onLinkClicked()`), which can be overridden by clients wanting to change default behavior. +> The `DefaultCheckoutEventProcessor` provides default implementations for current and future callback functions (such as `onCheckoutLinkClicked()`), which can be overridden by clients wanting to change default behavior. ### Error handling diff --git a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutEventProcessor.kt b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutEventProcessor.kt index e01295ac..6a0a44d9 100644 --- a/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutEventProcessor.kt +++ b/lib/src/main/java/com/shopify/checkoutsheetkit/CheckoutEventProcessor.kt @@ -22,9 +22,11 @@ */ package com.shopify.checkoutsheetkit +import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import android.webkit.GeolocationPermissions import android.webkit.PermissionRequest @@ -35,99 +37,111 @@ import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent import com.shopify.checkoutsheetkit.pixelevents.PixelEvent /** - * Interface to implement to allow responding to lifecycle events in checkout. - * We'd strongly recommend extending DefaultCheckoutEventProcessor where possible + * Interface to implement to allow responding to lifecycle events in checkout. We'd strongly + * recommend extending DefaultCheckoutEventProcessor where possible */ public interface CheckoutEventProcessor { - /** - * Event representing the successful completion of a checkout. - */ + /** Event representing the successful completion of a checkout. */ public fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) /** - * Event representing an error that occurred during checkout. This can be used to display - * error messages for example. + * Event representing an error that occurred during checkout. This can be used to display error + * messages for example. * - * @param error - the CheckoutErrorException that occurred + * @param error + * - the CheckoutErrorException that occurred * @see Exception */ public fun onCheckoutFailed(error: CheckoutException) - /** - * Event representing the cancellation/closing of checkout by the buyer - */ + /** Event representing the cancellation/closing of checkout by the buyer */ public fun onCheckoutCanceled() /** * Event indicating that a link has been clicked within checkout that should be opened outside - * of the WebView, e.g. in a system browser or email client. Protocols can be http/https/mailto/tel + * of the WebView, e.g. in a system browser or email client. Protocols can be + * http/https/mailto/tel */ public fun onCheckoutLinkClicked(uri: Uri) - /** - * A permission has been requested by the web chrome client, e.g. to access the camera - */ + /** 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 + * Web Pixel event emitted from checkout, that can be optionally transformed, enhanced (e.g. + * with user and session identifiers), and processed */ public fun onWebPixelEvent(event: PixelEvent) /** - * Called when the client should show a file chooser. This is called to handle HTML forms with 'file' input type, in response to the - * user pressing the "Select File" button. To cancel the request, call filePathCallback.onReceiveValue(null) and return true. + * Called when the client should show a file chooser. This is called to handle HTML forms with + * 'file' input type, in response to the user pressing the "Select File" button. To cancel the + * request, call filePathCallback.onReceiveValue(null) and return true. */ public fun onShowFileChooser( - webView: WebView, - filePathCallback: ValueCallback>, - fileChooserParams: WebChromeClient.FileChooserParams, + webView: WebView, + filePathCallback: ValueCallback>, + fileChooserParams: WebChromeClient.FileChooserParams, ): Boolean /** - * Called when the client should show a location permissions prompt. For example when using 'Use my location' for - * pickup points in checkout + * Called when the client should show a location permissions prompt. For example when using 'Use + * my location' for pickup points in checkout */ - public fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) + public fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) /** - * Called when the client should hide the location permissions prompt, e.g. if th request is cancelled + * Called when the client should hide the location permissions prompt, e.g. if th request is + * cancelled */ public fun onGeolocationPermissionsHidePrompt() } internal class NoopEventProcessor : CheckoutEventProcessor { - override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {/* noop */ + override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) { + /* noop */ } - override fun onCheckoutFailed(error: CheckoutException) {/* noop */ + override fun onCheckoutFailed(error: CheckoutException) { + /* noop */ } - override fun onCheckoutCanceled() {/* noop */ + override fun onCheckoutCanceled() { + /* noop */ } - override fun onCheckoutLinkClicked(uri: Uri) {/* noop */ + override fun onCheckoutLinkClicked(uri: Uri) { + /* noop */ } - override fun onWebPixelEvent(event: PixelEvent) {/* noop */ + override fun onWebPixelEvent(event: PixelEvent) { + /* noop */ } override fun onShowFileChooser( - webView: WebView, - filePathCallback: ValueCallback>, - fileChooserParams: WebChromeClient.FileChooserParams, + webView: WebView, + filePathCallback: ValueCallback>, + fileChooserParams: WebChromeClient.FileChooserParams, ): Boolean { return false } - override fun onPermissionRequest(permissionRequest: PermissionRequest) {/* noop */ + override fun onPermissionRequest(permissionRequest: PermissionRequest) { + /* noop */ } - override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {/* noop */ + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + /* noop */ } - override fun onGeolocationPermissionsHidePrompt() {/* noop */ + override fun onGeolocationPermissionsHidePrompt() { + /* noop */ } } @@ -136,11 +150,19 @@ internal class NoopEventProcessor : CheckoutEventProcessor { * for handling checkout events and interacting with the Android operating system. * @param context from which we will launch intents. */ -public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor( - private val context: Context, - private val log: LogWrapper = LogWrapper(), +public abstract class DefaultCheckoutEventProcessor +@JvmOverloads +constructor( + private val context: Context, + private val log: LogWrapper = LogWrapper(), ) : CheckoutEventProcessor { + private val locationPermissions: Array = + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + override fun onCheckoutLinkClicked(uri: Uri) { when (uri.scheme) { "tel" -> context.launchPhoneApp(uri.schemeSpecificPart) @@ -159,21 +181,63 @@ public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor( } override fun onShowFileChooser( - webView: WebView, - filePathCallback: ValueCallback>, - fileChooserParams: WebChromeClient.FileChooserParams, + webView: WebView, + filePathCallback: ValueCallback>, + fileChooserParams: WebChromeClient.FileChooserParams, ): Boolean { return false } - override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) { - // no-op override to implement + /** + * Called when the webview requests location permissions. For example when using 'Use my + * location' to locate pickup points. The default implementation here will check for both + * manifest and runtime permissions. If both have been granted, permission will be granted to + * present the location prompt to the user. + * + * Runtime permissions must be requested by your host app. The Checkout Sheet kit cannot request + * them on your behalf. + */ + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + // Check manifest permissions + val manifestPermissions = + try { + context.packageManager + .getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + .requestedPermissions + ?.toSet() + ?: emptySet() + } catch (e: Exception) { + log.e(TAG, "Failed to get package permissions", e) + emptySet() + } + + // Check if either permission is declared in manifest + val hasManifestPermission = + locationPermissions.any { permission -> manifestPermissions.contains(permission) } + + if (!hasManifestPermission) { + callback.invoke(origin, false, false) + return + } + + // Check runtime permissions + val hasRuntimePermission = + locationPermissions.any { permission -> + context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED + } + + callback.invoke(origin, hasRuntimePermission, hasRuntimePermission) } override fun onGeolocationPermissionsHidePrompt() { // no-op override to implement } + // Private + private fun Context.launchEmailApp(to: String) { val intent = Intent(Intent.ACTION_SEND) intent.type = "vnd.android.cursor.item/email" diff --git a/lib/src/test/java/com/shopify/checkoutsheetkit/DefaultCheckoutEventProcessorTest.kt b/lib/src/test/java/com/shopify/checkoutsheetkit/DefaultCheckoutEventProcessorTest.kt index c0327579..d4b6dba9 100644 --- a/lib/src/test/java/com/shopify/checkoutsheetkit/DefaultCheckoutEventProcessorTest.kt +++ b/lib/src/test/java/com/shopify/checkoutsheetkit/DefaultCheckoutEventProcessorTest.kt @@ -1,18 +1,18 @@ /* * 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 @@ -22,19 +22,23 @@ */ package com.shopify.checkoutsheetkit +import android.Manifest +import android.content.Context import android.content.Intent +import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.net.Uri +import android.webkit.GeolocationPermissions import androidx.activity.ComponentActivity import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent -import com.shopify.checkoutsheetkit.pixelevents.PixelEvent import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.whenever import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @@ -44,13 +48,20 @@ import org.robolectric.shadows.ShadowActivity @RunWith(RobolectricTestRunner::class) class DefaultCheckoutEventProcessorTest { + private lateinit var context: Context private lateinit var activity: ComponentActivity private lateinit var shadowActivity: ShadowActivity + private val mockCallback = mock() + private lateinit var packageManager: PackageManager @Before fun setUp() { activity = Robolectric.buildActivity(ComponentActivity::class.java).get() shadowActivity = shadowOf(activity) + context = mock() + packageManager = mock() + whenever(context.packageManager).thenReturn(packageManager) + whenever(context.packageName).thenReturn("com.test.package") } @Test @@ -67,22 +78,21 @@ class DefaultCheckoutEventProcessorTest { @Test fun `onCheckoutLinkClicked with mailto scheme launches email intent with to address`() { - val processor = processor(activity) val uri = Uri.parse("mailto:test.user@shopify.com") - processor.onCheckoutLinkClicked(uri) + processor(activity).onCheckoutLinkClicked(uri) val intent = shadowActivity.peekNextStartedActivityForResult().intent - assertThat(intent.getStringArrayExtra(Intent.EXTRA_EMAIL)).isEqualTo(arrayOf("test.user@shopify.com")) + assertThat(intent.getStringArrayExtra(Intent.EXTRA_EMAIL)) + .isEqualTo(arrayOf("test.user@shopify.com")) assertThat(intent.action).isEqualTo("android.intent.action.SEND") } @Test fun `onCheckoutLinkClicked with tel scheme launches action dial intent with phone number`() { - val processor = processor(activity) val uri = Uri.parse("tel:0123456789") - processor.onCheckoutLinkClicked(uri) + processor(activity).onCheckoutLinkClicked(uri) val intent = shadowActivity.peekNextStartedActivityForResult().intent assertThat(intent.data).isEqualTo(uri) @@ -101,14 +111,7 @@ class DefaultCheckoutEventProcessorTest { val shadowPackageManager = shadowOf(pm) shadowPackageManager.addResolveInfoForIntent(expectedIntent, ResolveInfo()) - val processor = object: DefaultCheckoutEventProcessor(activity, log) { - override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {/* not implemented */} - override fun onCheckoutFailed(error: CheckoutException) {/* not implemented */} - override fun onCheckoutCanceled() {/* not implemented */} - override fun onWebPixelEvent(event: PixelEvent) {/* not implemented */} - } - - processor.onCheckoutLinkClicked(uri) + processor(activity, log).onCheckoutLinkClicked(uri) val intent = shadowActivity.peekNextStartedActivityForResult().intent assertThat(intent.data).isEqualTo(uri) @@ -118,19 +121,16 @@ class DefaultCheckoutEventProcessorTest { @Test fun `onCheckoutLinkedClick with unhandled scheme logs warning`() { val log = mock() - val processor = object: DefaultCheckoutEventProcessor(activity, log) { - override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {/* not implemented */} - override fun onCheckoutFailed(error: CheckoutException) {/* not implemented */} - override fun onCheckoutCanceled() {/* not implemented */} - override fun onWebPixelEvent(event: PixelEvent) {/* not implemented */} - } - val uri = Uri.parse("ftp:random") - processor.onCheckoutLinkClicked(uri) + processor(activity, log).onCheckoutLinkClicked(uri) assertThat(shadowActivity.peekNextStartedActivityForResult()).isNull() - verify(log).w("DefaultCheckoutEventProcessor", "Unrecognized scheme for link clicked in checkout 'ftp:random'") + verify(log) + .w( + "DefaultCheckoutEventProcessor", + "Unrecognized scheme for link clicked in checkout 'ftp:random'" + ) } @Test @@ -139,21 +139,11 @@ class DefaultCheckoutEventProcessorTest { var description = "" var recoverable: Boolean? = null val processor = - object : DefaultCheckoutEventProcessor(activity, log) { - override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) { - /* not implemented */ - } + object : TestCheckoutEventProcessor(activity, log) { override fun onCheckoutFailed(error: CheckoutException) { description = error.errorDescription recoverable = error.isRecoverable } - override fun onCheckoutCanceled() { - /* not implemented */ - } - - override fun onWebPixelEvent(event: PixelEvent) { - /* not implemented */ - } } val error = object : CheckoutUnavailableException("error description", "unknown", true) {} @@ -164,12 +154,86 @@ class DefaultCheckoutEventProcessorTest { assertThat(recoverable).isTrue() } - private fun processor(activity: ComponentActivity): DefaultCheckoutEventProcessor { - return object: DefaultCheckoutEventProcessor(activity) { - override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {/* not implemented */} - override fun onCheckoutFailed(error: CheckoutException) {/* not implemented */} - override fun onCheckoutCanceled() {/* not implemented */} - override fun onWebPixelEvent(event: PixelEvent) {/* not implemented */} + @Test + fun `invokes the callback with false for grant and retain arguments`() { + // Simulate no permissions declared in the manifest + whenever(packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)) + .thenThrow(PackageManager.NameNotFoundException()) + processor(activity).onGeolocationPermissionsShowPrompt("http://shopify.com", mockCallback) + verify(mockCallback).invoke("http://shopify.com", false, false) + } + + @Test + fun `invokes the callback with true for grant and retain arguments when included in the manifest`() { + // Simulate permissions declared in the manifest + val permissions = + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + whenever(packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)) + .thenReturn(mockPackageInfo(permissions)) + + // Simulate runtime permission granted + whenever(context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)) + .thenReturn(PackageManager.PERMISSION_GRANTED) + whenever(context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) + .thenReturn(PackageManager.PERMISSION_GRANTED) + + TestCheckoutEventProcessor(context) + .onGeolocationPermissionsShowPrompt("http://shopify.com", mockCallback) + + verify(mockCallback).invoke("http://shopify.com", true, true) + } + + @Test + fun `invokes the callback with false for grant and retain arguments when not included in the manifest`() { + // Simulate permissions declared in the manifest + val permissions = + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + whenever(packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)) + .thenReturn(mockPackageInfo(permissions)) + + // Simulate runtime permission denied + whenever(context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)) + .thenReturn(PackageManager.PERMISSION_DENIED) + whenever(context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) + .thenReturn(PackageManager.PERMISSION_DENIED) + + TestCheckoutEventProcessor(context) + .onGeolocationPermissionsShowPrompt("http://shopify.com", mockCallback) + + verify(mockCallback).invoke("http://shopify.com", false, false) + } + + // Private + + private fun processor( + activity: ComponentActivity, + log: LogWrapper = LogWrapper() + ): DefaultCheckoutEventProcessor { + return object : TestCheckoutEventProcessor(activity, log) {} + } + + private fun mockPackageInfo(permissions: Array): PackageInfo { + return PackageInfo().apply { requestedPermissions = permissions } + } + + private open class TestCheckoutEventProcessor( + context: Context, + log: LogWrapper = LogWrapper() + ) : DefaultCheckoutEventProcessor(context, log) { + override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) { + /*noop*/ + } + override fun onCheckoutFailed(error: CheckoutException) { + /*noop*/ + } + override fun onCheckoutCanceled() { + /*noop*/ } } } diff --git a/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/MainActivity.kt b/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/MainActivity.kt index 9898c8e9..58bcc029 100644 --- a/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/MainActivity.kt +++ b/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/MainActivity.kt @@ -1,18 +1,18 @@ /* * 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 @@ -26,7 +26,6 @@ import android.Manifest import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle -import android.webkit.GeolocationPermissions import android.webkit.ValueCallback import android.webkit.WebChromeClient.FileChooserParams import android.webkit.WebView.setWebContentsDebuggingEnabled @@ -34,7 +33,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import timber.log.Timber import timber.log.Timber.DebugTree @@ -42,15 +40,15 @@ class MainActivity : ComponentActivity() { // Launchers private lateinit var requestPermissionLauncher: ActivityResultLauncher private lateinit var showFileChooserLauncher: ActivityResultLauncher - private lateinit var geolocationLauncher: ActivityResultLauncher> - // State related to file chooser requests (e.g. for using a file chooser/camera for proving identity) + // State related to file chooser requests (e.g. for using a file chooser/camera for proving + // identity) private var filePathCallback: ValueCallback>? = null private var fileChooserParams: FileChooserParams? = null - // State related to geolocation requests (e.g. for pickup points - use my location) - private var geolocationPermissionCallback: GeolocationPermissions.Callback? = null - private var geolocationOrigin: String? = null + companion object { + private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -63,43 +61,53 @@ class MainActivity : ComponentActivity() { Timber.plant(DebugTree()) } - setContent { - CheckoutSdkApp() - } - - requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> - val fileChooserParams = this.fileChooserParams - if (isGranted && fileChooserParams != null) { - this.showFileChooserLauncher.launch(fileChooserParams) - this.fileChooserParams = null - } - // N.B. a file chooser intent (without camera) could be launched here if the permission was denied - } - - showFileChooserLauncher = registerForActivityResult(FileChooserResultContract()) { uri: Uri? -> - // invoke the callback with the selected file - filePathCallback?.onReceiveValue(if (uri != null) arrayOf(uri) else null) - - // reset fileChooser state - filePathCallback = null - fileChooserParams = null - } - - geolocationLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> - val isGranted = result.any { it.value } - // invoke the callback with the permission result - geolocationPermissionCallback?.invoke(geolocationOrigin, isGranted, false) + setContent { CheckoutSdkApp() } + + requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted + -> + val fileChooserParams = this.fileChooserParams + if (isGranted && fileChooserParams != null) { + this.showFileChooserLauncher.launch(fileChooserParams) + this.fileChooserParams = null + } + // N.B. a file chooser intent (without camera) could be launched here if the + // permission was denied + } + + showFileChooserLauncher = + registerForActivityResult(FileChooserResultContract()) { uri: Uri? -> + // invoke the callback with the selected file + filePathCallback?.onReceiveValue(if (uri != null) arrayOf(uri) else null) + + // reset fileChooser state + filePathCallback = null + fileChooserParams = null + } + } - // reset geolocation state - geolocationPermissionCallback = null - geolocationOrigin = null + fun requestGeolocationPermission() { + val fineLocationGranted = permissionGranted(Manifest.permission.ACCESS_FINE_LOCATION) + val coarseLocationGranted = permissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION) + + if (!fineLocationGranted && !coarseLocationGranted) { + requestPermissions( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ), + LOCATION_PERMISSION_REQUEST_CODE + ) } } // Show a file chooser when prompted by the event processor - fun onShowFileChooser(filePathCallback: ValueCallback>, fileChooserParams: FileChooserParams): Boolean { + fun onShowFileChooser( + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams + ): Boolean { this.filePathCallback = filePathCallback - if (permissionAlreadyGranted(Manifest.permission.CAMERA)) { + if (permissionGranted(Manifest.permission.CAMERA)) { // Permissions already granted, launch chooser immediately showFileChooserLauncher.launch(fileChooserParams) this.fileChooserParams = null @@ -111,20 +119,7 @@ class MainActivity : ComponentActivity() { return true } - // Deal with requests from Checkout to show the geolocation permissions prompt - fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) { - if (permissionAlreadyGranted(Manifest.permission.ACCESS_FINE_LOCATION) && permissionAlreadyGranted(Manifest.permission.ACCESS_COARSE_LOCATION)) { - // Permissions already granted, invoke callback immediately - callback(origin, true, true) - } else { - // Permissions not yet granted, request permissions before invoking callback - geolocationPermissionCallback = callback - geolocationOrigin = origin - geolocationLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)) - } - } - - private fun permissionAlreadyGranted(permission: String): Boolean { - return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED + private fun permissionGranted(permission: String): Boolean { + return checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED } } diff --git a/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/common/MobileBuyEventProcessor.kt b/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/common/MobileBuyEventProcessor.kt index 7e0153b9..919dbfcb 100644 --- a/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/common/MobileBuyEventProcessor.kt +++ b/samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/common/MobileBuyEventProcessor.kt @@ -85,7 +85,8 @@ class MobileBuyEventProcessor( } override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) { - return (context as MainActivity).onGeolocationPermissionsShowPrompt(origin, callback) + (context as MainActivity).requestGeolocationPermission() + super.onGeolocationPermissionsShowPrompt(origin, callback) } override fun onShowFileChooser(