Skip to content

Commit 3921fb8

Browse files
fix(rn): align error payloads with native SDK APIs
1 parent a4ddaa4 commit 3921fb8

16 files changed

Lines changed: 805 additions & 448 deletions

File tree

platforms/react-native/README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ shopify.present(checkoutUrl, {
601601
// The sheet was dismissed without a terminal error
602602
},
603603
onFail: (error: CheckoutException) => {
604-
// A terminal error occurred — inspect `error.code`, `error.recoverable`, etc.
604+
// A terminal error occurred — inspect `error.code`, `error.message`, etc.
605605
},
606606
});
607607
```
@@ -739,9 +739,9 @@ behalf.
739739
The geolocation request flow follows this sequence:
740740

741741
1. When checkout needs location data (e.g., to show nearby pickup points), it triggers a geolocation request.
742-
2. If you've passed an `onGeolocationRequest` callback to `present()`, that callback is invoked.
742+
2. If you've passed an `onGeolocationRequest` callback to `present()`, that callback is invoked. Request or check Android permissions, then call `event.respond(allow)`.
743743
3. Otherwise, with `features.handleGeolocationRequests: true` (the default), the module automatically handles the Android runtime permission request.
744-
4. The result is passed back to checkout, which then proceeds to show relevant pickup points if permission was granted.
744+
4. The response is passed back to checkout, which then proceeds to show relevant pickup points if permission was granted.
745745

746746
> [!NOTE]
747747
> 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.
@@ -751,13 +751,14 @@ The geolocation request flow follows this sequence:
751751
> [!NOTE]
752752
> This section is only applicable for Android.
753753

754-
There are two ways to opt out, depending on whether you want to override the
755-
behavior for every presentation or just one.
754+
There are two ways to customize Android geolocation handling, depending on
755+
whether you want to override the behavior for one presentation or disable the
756+
fallback globally.
756757

757758
**Per-call override.** Pass an `onGeolocationRequest` callback to
758759
`present()`. When set, the callback fires instead of the default handler
759760
for that one presentation; the consumer is responsible for resolving
760-
permissions and calling `initiateGeolocationRequest(allow)`:
761+
permissions and calling `event.respond(allow)`:
761762

762763
```tsx
763764
shopify.present(checkoutUrl, {
@@ -769,16 +770,19 @@ shopify.present(checkoutUrl, {
769770
const granted =
770771
results[coarse] === 'granted' || results[fine] === 'granted';
771772

772-
shopify.initiateGeolocationRequest(granted);
773+
event.respond(granted);
773774
},
774775
});
775776
```
776777

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.
778+
`event.respond(...)` resolves checkout's pending WebView geolocation request.
779+
It does not request OS permissions by itself.
780+
781+
**Process-wide default-handler opt-out.** Set
782+
`features.handleGeolocationRequests` to `false` when you instantiate the
783+
`ShopifyCheckout` class to disable the default handler entirely. When this is
784+
set, pass `onGeolocationRequest` to any `present()` call that may need
785+
geolocation; otherwise the checkout geolocation request will not be resolved.
782786

783787
```tsx
784788
const shopifyCheckout = new ShopifyCheckout(config, {handleGeolocationRequests: false});

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,17 @@ const exampleConfig = {
5252

5353
const ShopifyCheckoutKit = {
5454
version: '0.7.0',
55-
getConstants: jest.fn(() => ({version: '0.7.0'})),
55+
getConstants: jest.fn(() => ({
56+
version: '0.7.0',
57+
dispatchEventTypes: ['close', 'fail', 'geolocationRequest'],
58+
})),
5659
preload: jest.fn(),
5760
present: jest.fn(),
5861
dismiss: jest.fn(),
5962
invalidateCache: jest.fn(),
6063
getConfig: jest.fn(() => exampleConfig),
6164
setConfig: jest.fn(),
62-
initiateGeolocationRequest: jest.fn(),
65+
respondToGeolocationRequest: jest.fn(),
6366
configureAcceleratedCheckouts: jest.fn(() => true),
6467
isAcceleratedCheckoutAvailable: jest.fn(() => true),
6568
isApplePayAvailable: jest.fn(() => true),

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

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ of this software and associated documentation files (the "Software"), to deal
2323

2424
package com.shopify.reactnative.checkoutkit;
2525

26-
import android.content.Context;
2726
import android.util.Log;
2827
import android.webkit.GeolocationPermissions;
2928

@@ -32,16 +31,15 @@ of this software and associated documentation files (the "Software"), to deal
3231

3332
import com.shopify.checkoutkit.*;
3433
import com.facebook.react.bridge.Callback;
35-
import com.facebook.react.modules.core.DeviceEventManagerModule;
36-
import com.facebook.react.bridge.ReactApplicationContext;
3734
import com.fasterxml.jackson.databind.ObjectMapper;
3835
import com.fasterxml.jackson.databind.node.ObjectNode;
3936
import java.io.IOException;
4037
import java.util.HashMap;
4138
import java.util.Map;
4239

4340
public class CustomCheckoutListener extends DefaultCheckoutListener {
44-
private final ReactApplicationContext reactContext;
41+
private static final String TAG = "ShopifyCheckoutKit";
42+
4543
private final ObjectMapper mapper = new ObjectMapper();
4644

4745
@Nullable
@@ -52,9 +50,7 @@ public class CustomCheckoutListener extends DefaultCheckoutListener {
5250
private String geolocationOrigin;
5351
private GeolocationPermissions.Callback geolocationCallback;
5452

55-
public CustomCheckoutListener(Context context, ReactApplicationContext reactContext,
56-
@Nullable Callback dispatch) {
57-
this.reactContext = reactContext;
53+
public CustomCheckoutListener(@Nullable Callback dispatch) {
5854
this.dispatchCallback = dispatch;
5955
}
6056

@@ -68,6 +64,12 @@ public void invokeGeolocationCallback(boolean allow) {
6864
}
6965
}
7066

67+
public void release() {
68+
dispatchCallback = null;
69+
geolocationCallback = null;
70+
geolocationOrigin = null;
71+
}
72+
7173
// Lifecycle events
7274

7375
/**
@@ -88,14 +90,18 @@ public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
8890
this.geolocationOrigin = origin;
8991

9092
if (dispatchCallback == null) {
93+
// Multi-shot geolocation requests can in principle arrive after a
94+
// terminal event has nulled the dispatcher. Log so the silence is
95+
// observable rather than mystifying.
96+
Log.w(TAG, "Dropping geolocationRequest \u2014 dispatcher already released by a terminal event.");
9197
return;
9298
}
9399
try {
94100
Map<String, Object> payload = new HashMap<>();
95101
payload.put("origin", origin);
96-
dispatchCallback.invoke(buildEnvelope("geolocationRequest", payload));
102+
dispatchCallback.invoke(buildEnvelope(DispatchEventTypes.GEOLOCATION_REQUEST, payload));
97103
} catch (IOException e) {
98-
Log.e("ShopifyCheckoutKit", "Error emitting \"geolocationRequest\" event", e);
104+
Log.e(TAG, "Error emitting \"geolocationRequest\" event", e);
99105
}
100106
}
101107

@@ -109,29 +115,33 @@ public void onGeolocationPermissionsHidePrompt() {
109115

110116
@Override
111117
public void onCheckoutFailed(CheckoutException checkoutError) {
112-
if (dispatchCallback == null) {
118+
Callback dispatch = dispatchCallback;
119+
if (dispatch == null) {
120+
release();
113121
return;
114122
}
115123
try {
116-
dispatchCallback.invoke(buildEnvelope("fail", populateErrorDetails(checkoutError)));
124+
dispatch.invoke(buildEnvelope(DispatchEventTypes.FAIL, populateErrorDetails(checkoutError)));
117125
} catch (IOException e) {
118-
Log.e("ShopifyCheckoutKit", "Error processing checkout failed event", e);
126+
Log.e(TAG, "Error processing checkout failed event", e);
119127
} finally {
120-
dispatchCallback = null;
128+
release();
121129
}
122130
}
123131

124132
@Override
125133
public void onCheckoutCanceled() {
126-
if (dispatchCallback == null) {
134+
Callback dispatch = dispatchCallback;
135+
if (dispatch == null) {
136+
release();
127137
return;
128138
}
129139
try {
130-
dispatchCallback.invoke(buildEnvelope("close", null));
140+
dispatch.invoke(buildEnvelope(DispatchEventTypes.CLOSE, null));
131141
} catch (IOException e) {
132-
Log.e("ShopifyCheckoutKit", "Error processing checkout canceled event", e);
142+
Log.e(TAG, "Error processing checkout canceled event", e);
133143
} finally {
134-
dispatchCallback = null;
144+
release();
135145
}
136146
}
137147

@@ -150,7 +160,6 @@ private Map<String, Object> populateErrorDetails(CheckoutException checkoutError
150160
Map<String, Object> errorMap = new HashMap();
151161
errorMap.put("__typename", getErrorTypeName(checkoutError));
152162
errorMap.put("message", checkoutError.getErrorDescription());
153-
errorMap.put("recoverable", checkoutError.isRecoverable());
154163
errorMap.put("code", checkoutError.getErrorCode());
155164

156165
if (checkoutError instanceof HttpException) {
@@ -176,9 +185,4 @@ private String getErrorTypeName(CheckoutException error) {
176185
}
177186
}
178187

179-
private void sendEventWithStringData(String name, String data) {
180-
reactContext
181-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
182-
.emit(name, data);
183-
}
184188
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
MIT License
3+
4+
Copyright 2023 - Present, Shopify Inc.
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
*/
24+
25+
package com.shopify.reactnative.checkoutkit;
26+
27+
import java.util.Arrays;
28+
import java.util.Collections;
29+
import java.util.List;
30+
31+
/**
32+
* Canonical list of SDK lifecycle event types emitted by the
33+
* per-{@code present()} dispatcher.
34+
*
35+
* Mirrors {@code SDK_LIFECYCLE_EVENT_TYPES} in the JS package and
36+
* {@code DispatchEventType} on iOS. Exposed to JS via
37+
* {@code getTypedExportedConstants()} so the JS layer can verify the
38+
* two sides agree at construction time.
39+
*/
40+
public final class DispatchEventTypes {
41+
public static final String CLOSE = "close";
42+
public static final String FAIL = "fail";
43+
public static final String GEOLOCATION_REQUEST = "geolocationRequest";
44+
45+
public static final List<String> ALL = Collections.unmodifiableList(
46+
Arrays.asList(CLOSE, FAIL, GEOLOCATION_REQUEST));
47+
48+
private DispatchEventTypes() {}
49+
}

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public ShopifyCheckoutKitModule(ReactApplicationContext reactContext) {
6565
protected Map<String, Object> getTypedExportedConstants() {
6666
final Map<String, Object> constants = new HashMap<>();
6767
constants.put("version", ShopifyCheckoutKit.version);
68+
// Exposed so the JS layer can verify the SDK lifecycle event set
69+
// it was built against matches what this native module emits.
70+
constants.put("dispatchEventTypes", DispatchEventTypes.ALL);
6871
return constants;
6972
}
7073

@@ -80,24 +83,36 @@ public void removeListeners(double count) {
8083

8184
@ReactMethod
8285
public void present(String checkoutURL, @Nullable Callback dispatch) {
86+
releaseCheckoutListener();
87+
8388
Activity currentActivity = getCurrentActivity();
8489
if (currentActivity instanceof ComponentActivity) {
85-
checkoutListener = new CustomCheckoutListener(currentActivity, this.reactContext, dispatch);
90+
CustomCheckoutListener listener = new CustomCheckoutListener(dispatch);
91+
checkoutListener = listener;
8692
currentActivity.runOnUiThread(() -> {
8793
checkoutSheet = ShopifyCheckoutKit.present(checkoutURL, (ComponentActivity) currentActivity,
88-
checkoutListener);
94+
listener);
8995
});
9096
}
9197
}
9298

9399
@ReactMethod
94100
public void dismiss() {
101+
releaseCheckoutListener();
102+
95103
if (checkoutSheet != null) {
96104
checkoutSheet.dismiss();
97105
checkoutSheet = null;
98106
}
99107
}
100108

109+
private void releaseCheckoutListener() {
110+
if (checkoutListener != null) {
111+
checkoutListener.release();
112+
checkoutListener = null;
113+
}
114+
}
115+
101116
@ReactMethod(isBlockingSynchronousMethod = true)
102117
public WritableMap getConfig() {
103118
WritableMap resultConfig = Arguments.createMap();
@@ -170,7 +185,7 @@ public boolean isApplePayAvailable() {
170185
}
171186

172187
@ReactMethod
173-
public void initiateGeolocationRequest(boolean allow) {
188+
public void respondToGeolocationRequest(boolean allow) {
174189
if (checkoutListener != null) {
175190
checkoutListener.invokeGeolocationCallback(allow);
176191
}

0 commit comments

Comments
 (0)