Skip to content

Commit 866347c

Browse files
feat: remove addEventListener API
1 parent ca7804f commit 866347c

17 files changed

Lines changed: 512 additions & 576 deletions

File tree

platforms/react-native/README.md

Lines changed: 60 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ experiences.
4343
- [When to preload](#when-to-preload)
4444
- [Cache invalidation](#cache-invalidation)
4545
- [Checkout lifecycle](#checkout-lifecycle)
46-
- [`addEventListener(eventName, callback)`](#addeventlistenereventname-callback)
47-
- [`removeEventListeners(eventName)`](#removeeventlistenerseventname)
46+
- [SDK callbacks on `present()`](#sdk-callbacks-on-present)
4847
- [Identity \& customer accounts](#identity--customer-accounts)
4948
- [Cart: buyer bag, identity, and preferences](#cart-buyer-bag-identity-and-preferences)
5049
- [Multipass](#multipass)
@@ -590,60 +589,36 @@ Should you wish to manually clear the preload cache, there is a `ShopifyCheckout
590589

591590
## Checkout lifecycle
592591

593-
There are currently 3 checkout events exposed through the Native Module. You can
594-
subscribe to these events using `addEventListener` and `removeEventListeners`
595-
methods - available on both the context provider as well as the class instance.
592+
Lifecycle callbacks are passed per-call to `present()`. The bridge holds the
593+
handles for the duration of that one presentation and releases them on
594+
terminal events; nothing needs to be subscribed or torn down explicitly.
596595

597-
| Name | Callback | Description |
598-
| ----------- | ----------------------------------------- | ------------------------------------------------------------ |
599-
| `close` | `() => void` | Fired when the checkout has been closed. |
600-
| `completed` | `(event: CheckoutCompletedEvent) => void` | Fired when the checkout has been successfully completed. |
601-
| `error` | `(error: {message: string}) => void` | Fired when a checkout exception has been raised. |
602-
603-
### `addEventListener(eventName, callback)`
604-
605-
Subscribing to an event returns an `EmitterSubscription` object, which contains
606-
a `remove()` function to unsubscribe. Here's an example of how you might create
607-
an event listener in a React `useEffect`, ensuring to remove it on unmount.
596+
### SDK callbacks on `present()`
608597

609598
```tsx
610-
// Using hooks
611-
const shopifyCheckout = useShopifyCheckout();
612-
613-
useEffect(() => {
614-
const close = shopifyCheckout.addEventListener('close', () => {
615-
// Do something on checkout close
616-
});
617-
618-
const completed = shopifyCheckout.addEventListener(
619-
'completed',
620-
(event: CheckoutCompletedEvent) => {
621-
// Lookup order on checkout completion
622-
const orderId = event.orderDetails.id;
623-
},
624-
);
625-
626-
const error = shopifyCheckout.addEventListener(
627-
'error',
628-
(error: CheckoutError) => {
629-
// Do something on checkout error
630-
// console.log(error.message)
631-
},
632-
);
633-
634-
return () => {
635-
// It is important to clear the subscription on unmount to prevent memory leaks
636-
close?.remove();
637-
completed?.remove();
638-
error?.remove();
639-
};
640-
}, [shopifyCheckout]);
599+
shopify.present(checkoutUrl, {
600+
onClose: () => {
601+
// The sheet was dismissed without a terminal error
602+
},
603+
onFail: (error: CheckoutException) => {
604+
// A terminal error occurred — inspect `error.code`, `error.recoverable`, etc.
605+
},
606+
});
641607
```
642608

643-
### `removeEventListeners(eventName)`
609+
| Name | Callback | Fires |
610+
| ---------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
611+
| `onClose` | `() => void` | Once, when the buyer dismisses the sheet without a terminal error. |
612+
| `onFail` | `(error: CheckoutException) => void` | Once, when the checkout terminates with an error. |
613+
| `onGeolocationRequest` | `(event: GeolocationRequestEvent) => void` | Android only. Fired each time the webview requests geolocation permissions. See [Opting out of the default behavior](#opting-out-of-the-default-behavior). |
644614

645-
On the rare occasion that you want to remove all event listeners for a given
646-
`eventName`, you can use the `removeEventListeners(eventName)` method.
615+
`onClose` and `onFail` are mutually exclusive — exactly one of them fires
616+
per `present(...)` call, after which both handles are released.
617+
618+
> Protocol-level callbacks (`start`, `complete`, `error` on the protocol
619+
> client) are not part of this section and will land in a follow-up release
620+
> alongside a `<CheckoutSheet>` component. Checkout completion is not
621+
> currently surfaced through the per-call callbacks.
647622

648623
## Identity & customer accounts
649624

@@ -756,15 +731,16 @@ Android differs to iOS in that permission requests must be handled in two places
756731
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
757732
```
758733

759-
The Checkout Kit native module will emit a `geolocationRequest` event when the webview requests geolocation
760-
information. By default, the kit will listen for this event and request access to both coarse and fine access when
761-
invoked.
734+
When the webview requests geolocation information, the Checkout Kit native
735+
module surfaces it to JS so the app can respond. By default, the kit handles
736+
the request itself and asks for both coarse and fine access on the buyer's
737+
behalf.
762738

763739
The geolocation request flow follows this sequence:
764740

765741
1. When checkout needs location data (e.g., to show nearby pickup points), it triggers a geolocation request.
766-
2. The native module emits a `geolocationRequest` event.
767-
3. If using default behavior, the module automatically handles the Android runtime permission request.
742+
2. If you've passed an `onGeolocationRequest` callback to `present()`, that callback is invoked.
743+
3. Otherwise, with `features.handleGeolocationRequests: true` (the default), the module automatically handles the Android runtime permission request.
768744
4. The result is passed back to checkout, which then proceeds to show relevant pickup points if permission was granted.
769745

770746
> [!NOTE]
@@ -775,52 +751,53 @@ The geolocation request flow follows this sequence:
775751
> [!NOTE]
776752
> This section is only applicable for Android.
777753

778-
In order to opt-out of the default permission handling, you can set `features.handleGeolocationRequests` to `false`
779-
when you instantiate the `ShopifyCheckout` class.
754+
There are two ways to opt out, depending on whether you want to override the
755+
behavior for every presentation or just one.
780756

781-
If you're using the sheet programmatically, you can do so by specifying a `features` object as the second argument:
757+
**Per-call override.** Pass an `onGeolocationRequest` callback to
758+
`present()`. When set, the callback fires instead of the default handler
759+
for that one presentation; the consumer is responsible for resolving
760+
permissions and calling `initiateGeolocationRequest(allow)`:
761+
762+
```tsx
763+
shopify.present(checkoutUrl, {
764+
onGeolocationRequest: async (event: GeolocationRequestEvent) => {
765+
const coarse = 'android.permission.ACCESS_COARSE_LOCATION';
766+
const fine = 'android.permission.ACCESS_FINE_LOCATION';
767+
768+
const results = await PermissionsAndroid.requestMultiple([coarse, fine]);
769+
const granted =
770+
results[coarse] === 'granted' || results[fine] === 'granted';
771+
772+
shopify.initiateGeolocationRequest(granted);
773+
},
774+
});
775+
```
776+
777+
**Process-wide opt-out.** Set `features.handleGeolocationRequests` to
778+
`false` when you instantiate the `ShopifyCheckout` class to disable the
779+
default handler entirely. Use this if you intend to always handle
780+
geolocation yourself but don't want to wire the callback at every call
781+
site.
782782

783783
```tsx
784784
const shopifyCheckout = new ShopifyCheckout(config, {handleGeolocationRequests: false});
785785
```
786786

787-
If you're using the context provider, you can pass the same `features` object as a prop to the `ShopifyCheckoutProvider` component:
787+
If you're using the context provider, pass the same `features` object as a prop:
788788

789789
```tsx
790790
<ShopifyCheckoutProvider configuration={config} features={{handleGeolocationRequests: false}}>
791791
{children}
792792
</ShopifyCheckoutProvider>
793793
```
794794

795-
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:
795+
Custom permission handling lets you:
796796

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

801-
The steps here to implement your own logic are to:
802-
803-
1. Listen for the `geolocationRequest`
804-
2. Request the desired permissions
805-
3. Invoke the native callback by calling `initiateGeolocationRequest` with the permission status
806-
807-
```tsx
808-
// Listen for "geolocationRequest" events
809-
shopify.addEventListener('geolocationRequest', async (event: GeolocationRequestEvent) => {
810-
const coarse = 'android.permission.ACCESS_COARSE_LOCATION';
811-
const fine = 'android.permission.ACCESS_FINE_LOCATION';
812-
813-
// Request one or many permissions at once
814-
const results = await PermissionsAndroid.requestMultiple([coarse, fine]);
815-
816-
// Check the permission status results
817-
const permissionGranted = results[coarse] === 'granted' || results[fine] === 'granted';
818-
819-
// Dispatch an event to the native module to invoke the native callback with the permission status
820-
shopify.initiateGeolocationRequest(permissionGranted);
821-
})
822-
```
823-
824801
---
825802

826803
## Accelerated Checkouts

platforms/react-native/__mocks__/react-native.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ const ShopifyCheckoutKit = {
5656
invalidateCache: jest.fn(),
5757
getConfig: jest.fn(() => exampleConfig),
5858
setConfig: jest.fn(),
59-
addEventListener: jest.fn(),
60-
removeEventListeners: jest.fn(),
6159
initiateGeolocationRequest: jest.fn(),
6260
configureAcceleratedCheckouts: jest.fn(() => true),
6361
isAcceleratedCheckoutAvailable: jest.fn(() => true),

platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/CustomCheckoutEventProcessor.java

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ of this software and associated documentation files (the "Software"), to deal
3131
import androidx.annotation.Nullable;
3232

3333
import com.shopify.checkoutkit.*;
34+
import com.facebook.react.bridge.Callback;
3435
import com.facebook.react.modules.core.DeviceEventManagerModule;
35-
import com.facebook.react.bridge.WritableNativeMap;
3636
import com.facebook.react.bridge.ReactApplicationContext;
3737
import com.shopify.checkoutkit.lifecycleevents.CheckoutCompletedEvent;
3838
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -44,14 +44,26 @@ public class CustomCheckoutEventProcessor extends DefaultCheckoutEventProcessor
4444
private final ReactApplicationContext reactContext;
4545
private final ObjectMapper mapper = new ObjectMapper();
4646

47+
@Nullable
48+
private Callback onCloseCallback;
49+
@Nullable
50+
private Callback onFailCallback;
51+
@Nullable
52+
private Callback onGeolocationRequestCallback;
53+
4754
// Geolocation-specific variables
4855

4956
private String geolocationOrigin;
5057
private GeolocationPermissions.Callback geolocationCallback;
5158

52-
public CustomCheckoutEventProcessor(Context context, ReactApplicationContext reactContext) {
59+
public CustomCheckoutEventProcessor(Context context, ReactApplicationContext reactContext,
60+
@Nullable Callback onClose, @Nullable Callback onFail,
61+
@Nullable Callback onGeolocationRequest) {
5362
super(context);
5463
this.reactContext = reactContext;
64+
this.onCloseCallback = onClose;
65+
this.onFailCallback = onFail;
66+
this.onGeolocationRequestCallback = onGeolocationRequest;
5567
}
5668

5769
// Public methods
@@ -87,11 +99,15 @@ public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
8799
this.geolocationCallback = callback;
88100
this.geolocationOrigin = origin;
89101

90-
// Emit a "geolocationRequest" event to the app.
91102
try {
92103
Map<String, Object> event = new HashMap<>();
93104
event.put("origin", origin);
94-
sendEventWithStringData("geolocationRequest", mapper.writeValueAsString(event));
105+
String payload = mapper.writeValueAsString(event);
106+
if (onGeolocationRequestCallback != null) {
107+
onGeolocationRequestCallback.invoke(payload);
108+
} else {
109+
sendEventWithStringData("geolocationRequest", payload);
110+
}
95111
} catch (IOException e) {
96112
Log.e("ShopifyCheckoutKit", "Error emitting \"geolocationRequest\" event", e);
97113
}
@@ -108,17 +124,26 @@ public void onGeolocationPermissionsHidePrompt() {
108124

109125
@Override
110126
public void onCheckoutFailed(CheckoutException checkoutError) {
127+
if (onFailCallback == null) {
128+
return;
129+
}
111130
try {
112131
String data = mapper.writeValueAsString(populateErrorDetails(checkoutError));
113-
sendEventWithStringData("error", data);
132+
onFailCallback.invoke(data);
114133
} catch (IOException e) {
115134
Log.e("ShopifyCheckoutKit", "Error processing checkout failed event", e);
135+
} finally {
136+
onFailCallback = null;
116137
}
117138
}
118139

119140
@Override
120141
public void onCheckoutCanceled() {
121-
sendEvent("close", null);
142+
if (onCloseCallback == null) {
143+
return;
144+
}
145+
onCloseCallback.invoke();
146+
onCloseCallback = null;
122147
}
123148

124149
@Override
@@ -163,12 +188,6 @@ private String getErrorTypeName(CheckoutException error) {
163188
}
164189
}
165190

166-
private void sendEvent(String eventName, @Nullable WritableNativeMap params) {
167-
reactContext
168-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
169-
.emit(eventName, params);
170-
}
171-
172191
private void sendEventWithStringData(String name, String data) {
173192
reactContext
174193
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)

platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ of this software and associated documentation files (the "Software"), to deal
2727
import android.content.Context;
2828
import androidx.activity.ComponentActivity;
2929
import androidx.annotation.NonNull;
30+
import androidx.annotation.Nullable;
31+
import com.facebook.react.bridge.Callback;
3032
import com.facebook.react.bridge.Promise;
3133
import com.facebook.react.bridge.ReactApplicationContext;
3234
import com.facebook.react.bridge.ReactMethod;
@@ -80,10 +82,12 @@ public void removeListeners(double count) {
8082
}
8183

8284
@ReactMethod
83-
public void present(String checkoutURL) {
85+
public void present(String checkoutURL, @Nullable Callback onClose, @Nullable Callback onFail,
86+
@Nullable Callback onGeolocationRequest) {
8487
Activity currentActivity = getCurrentActivity();
8588
if (currentActivity instanceof ComponentActivity) {
86-
checkoutEventProcessor = new CustomCheckoutEventProcessor(currentActivity, this.reactContext);
89+
checkoutEventProcessor = new CustomCheckoutEventProcessor(currentActivity, this.reactContext, onClose,
90+
onFail, onGeolocationRequest);
8791
currentActivity.runOnUiThread(() -> {
8892
checkoutSheet = ShopifyCheckoutKit.present(checkoutURL, (ComponentActivity) currentActivity,
8993
checkoutEventProcessor);

platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit-Bridging-Header.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,5 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2323

2424
#import <React/RCTBridgeModule.h>
2525
#import <React/RCTViewManager.h>
26-
#import <React/RCTEventEmitter.h>
2726
#import <React/RCTUIManager.h>
2827
#import <React/RCTBridge.h>

platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ @interface RCT_EXTERN_MODULE (RCTShopifyCheckoutKit, NativeShopifyCheckoutKitSpe
4040

4141
RCT_EXTERN_METHOD(setConfig:(NSDictionary *)configuration)
4242

43+
RCT_EXTERN_METHOD(present:(NSString *)checkoutURL
44+
onClose:(RCTResponseSenderBlock)onClose
45+
onFail:(RCTResponseSenderBlock)onFail
46+
onGeolocationRequest:(RCTResponseSenderBlock)onGeolocationRequest)
47+
4348
@end
4449

4550
// TurboModule registration. `RCTModuleProviders` (generated by codegen from

0 commit comments

Comments
 (0)