Skip to content

Commit bb5d30e

Browse files
authored
[3.2.0] [Android] Handle geolocation requests (#148)
* Autogenerate manifest from env, update intents * Upgrade Android kit * WIP - Handle geolocation requests * Add tests * Minor fixes * Export types, update README * Bump: minor (3.2.0) + changelog entry
1 parent b1236df commit bb5d30e

File tree

12 files changed

+583
-78
lines changed

12 files changed

+583
-78
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.2.0 - December 18, 2024
4+
5+
- Handle geolocation requests for Android devices
6+
37
## 3.1.2 - November 4, 2024
48

59
- Add `consumerProguardRules` build.gradle option to prevent minification of

README.md

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ experiences.
3333
- [Colors](#colors)
3434
- [Localization](#localization)
3535
- [Checkout Sheet title](#checkout-sheet-title)
36-
- [iOS](#ios)
37-
- [Android](#android)
36+
- [iOS - Localization](#ios---localization)
37+
- [Android - Localization](#android---localization)
3838
- [Currency](#currency)
3939
- [Language](#language)
4040
- [Preloading](#preloading)
@@ -53,6 +53,10 @@ experiences.
5353
- [Customer Account API](#customer-account-api)
5454
- [Offsite Payments](#offsite-payments)
5555
- [Universal Links - iOS](#universal-links---ios)
56+
- [Pickup points / Pickup in store](#pickup-points--pickup-in-store)
57+
- [Geolocation - iOS](#geolocation---ios)
58+
- [Geolocation - Android](#geolocation---android)
59+
- [Opting out of the default behavior](#opting-out-of-the-default-behavior)
5660
- [Contributing](#contributing)
5761
- [License](#license)
5862

@@ -144,8 +148,8 @@ function App() {
144148
}
145149
```
146150

147-
See [Usage with the Storefront API](#usage-with-the-storefront-api) below on how
148-
to get a checkout URL to pass to the kit.
151+
See [usage with the Storefront API](#usage-with-the-storefront-api) below for details on how
152+
to obtain a checkout URL to pass to the kit.
149153

150154
> [!NOTE]
151155
> The recommended usage of the library is through a
@@ -434,9 +438,7 @@ function AppWithContext() {
434438

435439
#### Checkout Sheet title
436440

437-
There are several ways to change the title of the Checkout Sheet.
438-
439-
##### iOS
441+
##### iOS - Localization
440442

441443
On iOS, you can set a localized value on the `title` attribute of the
442444
configuration.
@@ -447,7 +449,7 @@ following:
447449
1. Create a `Localizable.xcstrings` file under "ios/{YourApplicationName}"
448450
2. Add an entry for the key `"shopify_checkout_sheet_title"`
449451

450-
##### Android
452+
##### Android - Localization
451453

452454
On Android, you can add a string entry for the key `"checkout_web_view_title"`
453455
to the "android/app/src/res/values/strings.xml" file for your application.
@@ -742,6 +744,95 @@ public func checkoutDidClickLink(url: URL) {
742744
}
743745
```
744746

747+
## Pickup points / Pickup in store
748+
749+
### Geolocation - iOS
750+
751+
Geolocation permission requests are handled out of the box by iOS, provided you've added the required location usage description to your `Info.plist` file:
752+
753+
```xml
754+
<key>NSLocationWhenInUseUsageDescription</key>
755+
<string>Your location is required to locate pickup points near you.</string>
756+
```
757+
758+
> [!TIP]
759+
> Consider also adding `NSLocationAlwaysAndWhenInUseUsageDescription` if your app needs background location access for other features.
760+
761+
### Geolocation - Android
762+
763+
Android differs to iOS in that permission requests must be handled in two places:
764+
(1) in your `AndroidManifest.xml` and (2) at runtime.
765+
766+
```xml
767+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
768+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
769+
```
770+
771+
The Checkout Sheet Kit native module will emit a `geolocationRequest` event when the webview requests geolocation
772+
information. By default, the kit will listen for this event and request access to both coarse and fine access when
773+
invoked.
774+
775+
The geolocation request flow follows this sequence:
776+
777+
1. When checkout needs location data (e.g., to show nearby pickup points), it triggers a geolocation request.
778+
2. The native module emits a `geolocationRequest` event.
779+
3. If using default behavior, the module automatically handles the Android runtime permission request.
780+
4. The result is passed back to checkout, which then proceeds to show relevant pickup points if permission was granted.
781+
782+
> [!NOTE]
783+
> If the user denies location permissions, the checkout will still function but will not be able to show nearby pickup points. Users can manually enter their location instead.
784+
785+
#### Opting out of the default behavior
786+
787+
> [!NOTE]
788+
> This section is only applicable for Android.
789+
790+
In order to opt-out of the default permission handling, you can set `features.handleGeolocationRequests` to `false`
791+
when you instantiate the `ShopifyCheckoutSheet` class.
792+
793+
If you're using the sheet programmatically, you can do so by specifying a `features` object as the second argument:
794+
795+
```tsx
796+
const checkoutSheetKit = new ShopifyCheckoutSheet(config, {handleGeolocationRequests: false});
797+
```
798+
799+
If you're using the context provider, you can pass the same `features` object as a prop to the `ShopifyCheckoutSheetProvider` component:
800+
801+
```tsx
802+
<ShopifyCheckoutSheetProvider configuration={config} features={{handleGeolocationRequests: false}}>
803+
{children}
804+
</ShopifyCheckoutSheetProvider>
805+
```
806+
807+
When opting out, you'll need to implement your own permission handling logic and communicate the result back to the checkout sheet. This can be useful if you want to:
808+
809+
- Customize the permission request UI/UX
810+
- Coordinate location permissions with other app features
811+
- Implement custom fallback behavior when permissions are denied
812+
813+
The steps here to implement your own logic are to:
814+
815+
1. Listen for the `geolocationRequest`
816+
2. Request the desired permissions
817+
3. Invoke the native callback by calling `initiateGeolocationRequest` with the permission status
818+
819+
```tsx
820+
// Listen for "geolocationRequest" events
821+
shopify.addEventListener('geolocationRequest', async (event: GeolocationRequestEvent) => {
822+
const coarse = 'android.permission.ACCESS_COARSE_LOCATION';
823+
const fine = 'android.permission.ACCESS_FINE_LOCATION';
824+
825+
// Request one or many permissions at once
826+
const results = await PermissionsAndroid.requestMultiple([coarse, fine]);
827+
828+
// Check the permission status results
829+
const permissionGranted = results[coarse] === 'granted' || results[fine] === 'granted';
830+
831+
// Dispatch an event to the native module to invoke the native callback with the permission status
832+
shopify.initiateGeolocationRequest(permissionGranted);
833+
})
834+
```
835+
745836
---
746837

747838
## Contributing

modules/@shopify/checkout-sheet-kit/android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ ndkVersion=23.1.7779620
55
buildToolsVersion = "33.0.0"
66

77
# Version of Shopify Checkout SDK to use with React Native
8-
SHOPIFY_CHECKOUT_SDK_VERSION=3.2.2
8+
SHOPIFY_CHECKOUT_SDK_VERSION=3.3.0

modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/CustomCheckoutEventProcessor.java

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ of this software and associated documentation files (the "Software"), to deal
2525

2626
import android.content.Context;
2727
import android.util.Log;
28+
import android.webkit.GeolocationPermissions;
29+
2830
import androidx.annotation.NonNull;
2931
import androidx.annotation.Nullable;
3032

@@ -41,25 +43,70 @@ of this software and associated documentation files (the "Software"), to deal
4143

4244
public class CustomCheckoutEventProcessor extends DefaultCheckoutEventProcessor {
4345
private final ReactApplicationContext reactContext;
44-
4546
private final ObjectMapper mapper = new ObjectMapper();
4647

48+
// Geolocation-specific variables
49+
50+
private String geolocationOrigin;
51+
private GeolocationPermissions.Callback geolocationCallback;
52+
4753
public CustomCheckoutEventProcessor(Context context, ReactApplicationContext reactContext) {
4854
super(context);
49-
5055
this.reactContext = reactContext;
5156
}
5257

58+
// Public methods
59+
60+
public void invokeGeolocationCallback(boolean allow) {
61+
if (geolocationCallback != null) {
62+
boolean retainGeolocationForFutureRequests = false;
63+
geolocationCallback.invoke(geolocationOrigin, allow, retainGeolocationForFutureRequests);
64+
geolocationCallback = null;
65+
}
66+
}
67+
68+
// Lifecycle events
69+
70+
/**
71+
* This method is called when the checkout sheet webpage requests geolocation
72+
* permissions.
73+
*
74+
* Since the app needs to request permissions first before granting, we store
75+
* the callback and origin in memory and emit a "geolocationRequest" event to
76+
* the app. The app will then request the necessary geolocation permissions
77+
* and invoke the native callback with the result.
78+
*
79+
* @param origin - The origin of the request
80+
* @param callback - The callback to invoke when the app requests permissions
81+
*/
5382
@Override
54-
public void onCheckoutCompleted(@NonNull CheckoutCompletedEvent event) {
83+
public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
84+
@NonNull GeolocationPermissions.Callback callback) {
85+
86+
// Store the callback and origin in memory. The kit will wait for the app to
87+
// request permissions first before granting.
88+
this.geolocationCallback = callback;
89+
this.geolocationOrigin = origin;
90+
91+
// Emit a "geolocationRequest" event to the app.
5592
try {
56-
String data = mapper.writeValueAsString(event);
57-
sendEventWithStringData("completed", data);
93+
Map<String, Object> event = new HashMap<>();
94+
event.put("origin", origin);
95+
sendEventWithStringData("geolocationRequest", mapper.writeValueAsString(event));
5896
} catch (IOException e) {
59-
Log.e("ShopifyCheckoutSheetKit", "Error processing completed event", e);
97+
Log.e("ShopifyCheckoutSheetKit", "Error emitting \"geolocationRequest\" event", e);
6098
}
6199
}
62100

101+
@Override
102+
public void onGeolocationPermissionsHidePrompt() {
103+
super.onGeolocationPermissionsHidePrompt();
104+
105+
// Reset the geolocation callback and origin when the prompt is hidden.
106+
this.geolocationCallback = null;
107+
this.geolocationOrigin = null;
108+
}
109+
63110
@Override
64111
public void onWebPixelEvent(@NonNull PixelEvent event) {
65112
try {
@@ -80,6 +127,23 @@ public void onCheckoutFailed(CheckoutException checkoutError) {
80127
}
81128
}
82129

130+
@Override
131+
public void onCheckoutCanceled() {
132+
sendEvent("close", null);
133+
}
134+
135+
@Override
136+
public void onCheckoutCompleted(@NonNull CheckoutCompletedEvent event) {
137+
try {
138+
String data = mapper.writeValueAsString(event);
139+
sendEventWithStringData("completed", data);
140+
} catch (IOException e) {
141+
Log.e("ShopifyCheckoutSheetKit", "Error processing completed event", e);
142+
}
143+
}
144+
145+
// Private
146+
83147
private Map<String, Object> populateErrorDetails(CheckoutException checkoutError) {
84148
Map<String, Object> errorMap = new HashMap();
85149
errorMap.put("__typename", getErrorTypeName(checkoutError));
@@ -110,11 +174,6 @@ private String getErrorTypeName(CheckoutException error) {
110174
}
111175
}
112176

113-
@Override
114-
public void onCheckoutCanceled() {
115-
sendEvent("close", null);
116-
}
117-
118177
private void sendEvent(String eventName, @Nullable WritableNativeMap params) {
119178
reactContext
120179
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)

0 commit comments

Comments
 (0)