Skip to content

Commit a64343d

Browse files
committed
Invoke geolocation callback by default
1 parent 65282a0 commit a64343d

File tree

5 files changed

+98
-36
lines changed

5 files changed

+98
-36
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ val processor = object : DefaultCheckoutEventProcessor(activity) {
351351
```
352352

353353
> [!Note]
354-
> 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.
354+
> 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.
355355

356356
### Error handling
357357

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
*/
2323
package com.shopify.checkoutsheetkit
2424

25+
import android.Manifest
2526
import android.annotation.SuppressLint
2627
import android.content.Context
2728
import android.content.Intent
29+
import android.content.pm.PackageManager
2830
import android.net.Uri
2931
import android.webkit.GeolocationPermissions
3032
import android.webkit.PermissionRequest
@@ -141,6 +143,11 @@ public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor(
141143
private val log: LogWrapper = LogWrapper(),
142144
) : CheckoutEventProcessor {
143145

146+
private val LOCATION_PERMISSIONS: Array<String> = arrayOf(
147+
Manifest.permission.ACCESS_FINE_LOCATION,
148+
Manifest.permission.ACCESS_COARSE_LOCATION
149+
)
150+
144151
override fun onCheckoutLinkClicked(uri: Uri) {
145152
when (uri.scheme) {
146153
"tel" -> context.launchPhoneApp(uri.schemeSpecificPart)
@@ -166,14 +173,51 @@ public abstract class DefaultCheckoutEventProcessor @JvmOverloads constructor(
166173
return false
167174
}
168175

169-
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
170-
// no-op override to implement
176+
/**
177+
* Called when the webview requests location permissions. For example when using 'Use my location' to locate pickup points.
178+
* The default implementation here will check for both manifest and runtime permissions. If both have been granted,
179+
* permission will be granted to present the location prompt to the user.
180+
*
181+
* Runtime permissions must be requested by your host app. The Checkout Sheet kit cannot request them on your behalf.
182+
*/
183+
override fun onGeolocationPermissionsShowPrompt(
184+
origin: String,
185+
callback: GeolocationPermissions.Callback
186+
) {
187+
// Check manifest permissions
188+
val manifestPermissions = try {
189+
context.packageManager
190+
.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
191+
.requestedPermissions
192+
?.toSet() ?: emptySet()
193+
} catch (e: Exception) {
194+
emptySet()
195+
}
196+
197+
// Check if either permission is declared in manifest
198+
val hasManifestPermission = LOCATION_PERMISSIONS.any { permission ->
199+
manifestPermissions.contains(permission)
200+
}
201+
202+
if (!hasManifestPermission) {
203+
callback.invoke(origin, false, false)
204+
return
205+
}
206+
207+
// Check runtime permissions
208+
val hasRuntimePermission = LOCATION_PERMISSIONS.any { permission ->
209+
context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
210+
}
211+
212+
callback.invoke(origin, hasRuntimePermission, hasRuntimePermission)
171213
}
172214

173215
override fun onGeolocationPermissionsHidePrompt() {
174216
// no-op override to implement
175217
}
176218

219+
// Private
220+
177221
private fun Context.launchEmailApp(to: String) {
178222
val intent = Intent(Intent.ACTION_SEND)
179223
intent.type = "vnd.android.cursor.item/email"

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/*
22
* MIT License
3-
*
3+
*
44
* Copyright 2023-present, Shopify Inc.
5-
*
5+
*
66
* Permission is hereby granted, free of charge, to any person obtaining a copy
77
* of this software and associated documentation files (the "Software"), to deal
88
* in the Software without restriction, including without limitation the rights
99
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1010
* copies of the Software, and to permit persons to whom the Software is
1111
* furnished to do so, subject to the following conditions:
12-
*
12+
*
1313
* The above copyright notice and this permission notice shall be included in all
1414
* copies or substantial portions of the Software.
15-
*
15+
*
1616
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1717
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1818
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -26,14 +26,17 @@ import android.content.Intent
2626
import android.content.pm.PackageManager
2727
import android.content.pm.ResolveInfo
2828
import android.net.Uri
29+
import android.webkit.GeolocationPermissions
2930
import androidx.activity.ComponentActivity
3031
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompletedEvent
3132
import com.shopify.checkoutsheetkit.pixelevents.PixelEvent
3233
import org.assertj.core.api.Assertions.assertThat
3334
import org.junit.Before
3435
import org.junit.Test
3536
import org.junit.runner.RunWith
37+
import org.mockito.kotlin.any
3638
import org.mockito.kotlin.mock
39+
import org.mockito.kotlin.times
3740
import org.mockito.kotlin.verify
3841
import org.robolectric.Robolectric
3942
import org.robolectric.RobolectricTestRunner
@@ -46,6 +49,7 @@ class DefaultCheckoutEventProcessorTest {
4649

4750
private lateinit var activity: ComponentActivity
4851
private lateinit var shadowActivity: ShadowActivity
52+
private val mockCallback = mock<GeolocationPermissions.Callback>()
4953

5054
@Before
5155
fun setUp() {
@@ -164,12 +168,36 @@ class DefaultCheckoutEventProcessorTest {
164168
assertThat(recoverable).isTrue()
165169
}
166170

171+
@Test
172+
fun `onGeolocationPermissionsShowPrompt should invoke callback with correct args`() {
173+
val origin = "http://shopify.com"
174+
val processor = object: DefaultCheckoutEventProcessor(mock(), mock()) {
175+
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {}
176+
override fun onCheckoutFailed(error: CheckoutException) {}
177+
override fun onCheckoutCanceled() {}
178+
}
179+
processor.onGeolocationPermissionsShowPrompt(origin, mockCallback)
180+
verify(mockCallback).invoke(origin, true, true)
181+
}
182+
183+
@Test
184+
fun `onGeolocationPermissionsShowPrompt should not invoke callback if overridden with no-op`() {
185+
val overriddenProcessor = object: DefaultCheckoutEventProcessor(mock(), mock()) {
186+
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {}
187+
override fun onCheckoutFailed(error: CheckoutException) {}
188+
override fun onCheckoutCanceled() {}
189+
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {}
190+
}
191+
overriddenProcessor.onGeolocationPermissionsShowPrompt("http://shopify.com", mockCallback)
192+
verify(mockCallback, times(0)).invoke(any(), any(), any())
193+
}
194+
167195
private fun processor(activity: ComponentActivity): DefaultCheckoutEventProcessor {
168196
return object: DefaultCheckoutEventProcessor(activity) {
169-
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {/* not implemented */}
170-
override fun onCheckoutFailed(error: CheckoutException) {/* not implemented */}
171-
override fun onCheckoutCanceled() {/* not implemented */}
172-
override fun onWebPixelEvent(event: PixelEvent) {/* not implemented */}
197+
override fun onCheckoutCompleted(checkoutCompletedEvent: CheckoutCompletedEvent) {}
198+
override fun onCheckoutFailed(error: CheckoutException) {}
199+
override fun onCheckoutCanceled() {}
200+
override fun onWebPixelEvent(event: PixelEvent) {}
173201
}
174202
}
175203
}

samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/MainActivity.kt

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ package com.shopify.checkout_sdk_mobile_buy_integration_sample
2525
import android.Manifest
2626
import android.content.pm.PackageManager
2727
import android.net.Uri
28+
import android.os.Build
2829
import android.os.Bundle
2930
import android.webkit.GeolocationPermissions
3031
import android.webkit.ValueCallback
@@ -42,15 +43,14 @@ class MainActivity : ComponentActivity() {
4243
// Launchers
4344
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
4445
private lateinit var showFileChooserLauncher: ActivityResultLauncher<FileChooserParams>
45-
private lateinit var geolocationLauncher: ActivityResultLauncher<Array<String>>
4646

4747
// State related to file chooser requests (e.g. for using a file chooser/camera for proving identity)
4848
private var filePathCallback: ValueCallback<Array<Uri>>? = null
4949
private var fileChooserParams: FileChooserParams? = null
5050

51-
// State related to geolocation requests (e.g. for pickup points - use my location)
52-
private var geolocationPermissionCallback: GeolocationPermissions.Callback? = null
53-
private var geolocationOrigin: String? = null
51+
companion object {
52+
private const val LOCATION_PERMISSION_REQUEST_CODE = 1001
53+
}
5454

5555
override fun onCreate(savedInstanceState: Bundle?) {
5656
super.onCreate(savedInstanceState)
@@ -84,15 +84,17 @@ class MainActivity : ComponentActivity() {
8484
filePathCallback = null
8585
fileChooserParams = null
8686
}
87+
}
8788

88-
geolocationLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
89-
val isGranted = result.any { it.value }
90-
// invoke the callback with the permission result
91-
geolocationPermissionCallback?.invoke(geolocationOrigin, isGranted, false)
89+
fun requestGeolocationPermission() {
90+
val fineLocationGranted = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
91+
val coarseLocationGranted = checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
9292

93-
// reset geolocation state
94-
geolocationPermissionCallback = null
95-
geolocationOrigin = null
93+
if (!fineLocationGranted && !coarseLocationGranted) {
94+
requestPermissions(
95+
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
96+
LOCATION_PERMISSION_REQUEST_CODE
97+
)
9698
}
9799
}
98100

@@ -111,19 +113,6 @@ class MainActivity : ComponentActivity() {
111113
return true
112114
}
113115

114-
// Deal with requests from Checkout to show the geolocation permissions prompt
115-
fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
116-
if (permissionAlreadyGranted(Manifest.permission.ACCESS_FINE_LOCATION) && permissionAlreadyGranted(Manifest.permission.ACCESS_COARSE_LOCATION)) {
117-
// Permissions already granted, invoke callback immediately
118-
callback(origin, true, true)
119-
} else {
120-
// Permissions not yet granted, request permissions before invoking callback
121-
geolocationPermissionCallback = callback
122-
geolocationOrigin = origin
123-
geolocationLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION))
124-
}
125-
}
126-
127116
private fun permissionAlreadyGranted(permission: String): Boolean {
128117
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
129118
}

samples/MobileBuyIntegration/app/src/main/java/com/shopify/checkout_sdk_mobile_buy_integration_sample/common/MobileBuyEventProcessor.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ class MobileBuyEventProcessor(
8585
}
8686

8787
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
88-
return (context as MainActivity).onGeolocationPermissionsShowPrompt(origin, callback)
88+
(context as MainActivity).requestGeolocationPermission()
89+
super.onGeolocationPermissionsShowPrompt(origin, callback)
8990
}
9091

9192
override fun onShowFileChooser(

0 commit comments

Comments
 (0)