Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.2.0 - December 18, 2024

- Handle geolocation requests for Android devices

## 3.1.2 - November 4, 2024

- Add `consumerProguardRules` build.gradle option to prevent minification of
Expand Down
107 changes: 99 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ experiences.
- [Colors](#colors)
- [Localization](#localization)
- [Checkout Sheet title](#checkout-sheet-title)
- [iOS](#ios)
- [Android](#android)
- [iOS - Localization](#ios---localization)
- [Android - Localization](#android---localization)
- [Currency](#currency)
- [Language](#language)
- [Preloading](#preloading)
Expand All @@ -53,6 +53,10 @@ experiences.
- [Customer Account API](#customer-account-api)
- [Offsite Payments](#offsite-payments)
- [Universal Links - iOS](#universal-links---ios)
- [Pickup points / Pickup in store](#pickup-points--pickup-in-store)
- [Geolocation - iOS](#geolocation---ios)
- [Geolocation - Android](#geolocation---android)
- [Opting out of the default behavior](#opting-out-of-the-default-behavior)
- [Contributing](#contributing)
- [License](#license)

Expand Down Expand Up @@ -144,8 +148,8 @@ function App() {
}
```

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

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

#### Checkout Sheet title

There are several ways to change the title of the Checkout Sheet.

##### iOS
##### iOS - Localization

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

##### Android
##### Android - Localization

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

## Pickup points / Pickup in store

### Geolocation - iOS

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:

```xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is required to locate pickup points near you.</string>
```

> [!TIP]
> Consider also adding `NSLocationAlwaysAndWhenInUseUsageDescription` if your app needs background location access for other features.

### Geolocation - Android

Android differs to iOS in that permission requests must be handled in two places:
(1) in your `AndroidManifest.xml` and (2) at runtime.

```xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
```

The Checkout Sheet Kit native module will emit a `geolocationRequest` event when the webview requests geolocation
information. By default, the kit will listen for this event and request access to both coarse and fine access when
invoked.

The geolocation request flow follows this sequence:

1. When checkout needs location data (e.g., to show nearby pickup points), it triggers a geolocation request.
2. The native module emits a `geolocationRequest` event.
3. If using default behavior, the module automatically handles the Android runtime permission request.
4. The result is passed back to checkout, which then proceeds to show relevant pickup points if permission was granted.

> [!NOTE]
> 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.

#### Opting out of the default behavior

> [!NOTE]
> This section is only applicable for Android.

In order to opt-out of the default permission handling, you can set `features.handleGeolocationRequests` to `false`
when you instantiate the `ShopifyCheckoutSheet` class.

If you're using the sheet programmatically, you can do so by specifying a `features` object as the second argument:

```tsx
const checkoutSheetKit = new ShopifyCheckoutSheet(config, {handleGeolocationRequests: false});
```

If you're using the context provider, you can pass the same `features` object as a prop to the `ShopifyCheckoutSheetProvider` component:

```tsx
<ShopifyCheckoutSheetProvider configuration={config} features={{handleGeolocationRequests: false}}>
{children}
</ShopifyCheckoutSheetProvider>
```

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:

- Customize the permission request UI/UX
- Coordinate location permissions with other app features
- Implement custom fallback behavior when permissions are denied

The steps here to implement your own logic are to:

1. Listen for the `geolocationRequest`
2. Request the desired permissions
3. Invoke the native callback by calling `initiateGeolocationRequest` with the permission status

```tsx
// Listen for "geolocationRequest" events
shopify.addEventListener('geolocationRequest', async (event: GeolocationRequestEvent) => {
const coarse = 'android.permission.ACCESS_COARSE_LOCATION';
const fine = 'android.permission.ACCESS_FINE_LOCATION';

// Request one or many permissions at once
const results = await PermissionsAndroid.requestMultiple([coarse, fine]);

// Check the permission status results
const permissionGranted = results[coarse] === 'granted' || results[fine] === 'granted';

// Dispatch an event to the native module to invoke the native callback with the permission status
shopify.initiateGeolocationRequest(permissionGranted);
})
```

---

## Contributing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ ndkVersion=23.1.7779620
buildToolsVersion = "33.0.0"

# Version of Shopify Checkout SDK to use with React Native
SHOPIFY_CHECKOUT_SDK_VERSION=3.2.2
SHOPIFY_CHECKOUT_SDK_VERSION=3.3.0
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ of this software and associated documentation files (the "Software"), to deal

import android.content.Context;
import android.util.Log;
import android.webkit.GeolocationPermissions;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

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

public class CustomCheckoutEventProcessor extends DefaultCheckoutEventProcessor {
private final ReactApplicationContext reactContext;

private final ObjectMapper mapper = new ObjectMapper();

// Geolocation-specific variables

private String geolocationOrigin;
private GeolocationPermissions.Callback geolocationCallback;

public CustomCheckoutEventProcessor(Context context, ReactApplicationContext reactContext) {
super(context);

this.reactContext = reactContext;
}

// Public methods

public void invokeGeolocationCallback(boolean allow) {
if (geolocationCallback != null) {
boolean retainGeolocationForFutureRequests = false;
geolocationCallback.invoke(geolocationOrigin, allow, retainGeolocationForFutureRequests);
geolocationCallback = null;
}
}

// Lifecycle events

/**
* This method is called when the checkout sheet webpage requests geolocation
* permissions.
*
* Since the app needs to request permissions first before granting, we store
* the callback and origin in memory and emit a "geolocationRequest" event to
* the app. The app will then request the necessary geolocation permissions
* and invoke the native callback with the result.
*
* @param origin - The origin of the request
* @param callback - The callback to invoke when the app requests permissions
*/
@Override
public void onCheckoutCompleted(@NonNull CheckoutCompletedEvent event) {
public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
@NonNull GeolocationPermissions.Callback callback) {

// Store the callback and origin in memory. The kit will wait for the app to
// request permissions first before granting.
this.geolocationCallback = callback;
this.geolocationOrigin = origin;

// Emit a "geolocationRequest" event to the app.
try {
String data = mapper.writeValueAsString(event);
sendEventWithStringData("completed", data);
Map<String, Object> event = new HashMap<>();
event.put("origin", origin);
sendEventWithStringData("geolocationRequest", mapper.writeValueAsString(event));
} catch (IOException e) {
Log.e("ShopifyCheckoutSheetKit", "Error processing completed event", e);
Log.e("ShopifyCheckoutSheetKit", "Error emitting \"geolocationRequest\" event", e);
}
}

@Override
public void onGeolocationPermissionsHidePrompt() {
super.onGeolocationPermissionsHidePrompt();

// Reset the geolocation callback and origin when the prompt is hidden.
this.geolocationCallback = null;
this.geolocationOrigin = null;
}

@Override
public void onWebPixelEvent(@NonNull PixelEvent event) {
try {
Expand All @@ -80,6 +127,23 @@ public void onCheckoutFailed(CheckoutException checkoutError) {
}
}

@Override
public void onCheckoutCanceled() {
sendEvent("close", null);
}

@Override
public void onCheckoutCompleted(@NonNull CheckoutCompletedEvent event) {
try {
String data = mapper.writeValueAsString(event);
sendEventWithStringData("completed", data);
} catch (IOException e) {
Log.e("ShopifyCheckoutSheetKit", "Error processing completed event", e);
}
}

// Private

private Map<String, Object> populateErrorDetails(CheckoutException checkoutError) {
Map<String, Object> errorMap = new HashMap();
errorMap.put("__typename", getErrorTypeName(checkoutError));
Expand Down Expand Up @@ -110,11 +174,6 @@ private String getErrorTypeName(CheckoutException error) {
}
}

@Override
public void onCheckoutCanceled() {
sendEvent("close", null);
}

private void sendEvent(String eventName, @Nullable WritableNativeMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
Expand Down
Loading
Loading