Skip to content

Commit 2b6a147

Browse files
feat: explore dynamic dispatch for checkout delegate
1 parent 523f115 commit 2b6a147

9 files changed

Lines changed: 296 additions & 334 deletions

File tree

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

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ of this software and associated documentation files (the "Software"), to deal
3535
import com.facebook.react.modules.core.DeviceEventManagerModule;
3636
import com.facebook.react.bridge.ReactApplicationContext;
3737
import com.fasterxml.jackson.databind.ObjectMapper;
38+
import com.fasterxml.jackson.databind.node.ObjectNode;
3839
import java.io.IOException;
3940
import java.util.HashMap;
4041
import java.util.Map;
@@ -44,24 +45,17 @@ public class CustomCheckoutListener extends DefaultCheckoutListener {
4445
private final ObjectMapper mapper = new ObjectMapper();
4546

4647
@Nullable
47-
private Callback onCloseCallback;
48-
@Nullable
49-
private Callback onFailCallback;
50-
@Nullable
51-
private Callback onGeolocationRequestCallback;
48+
private Callback dispatchCallback;
5249

5350
// Geolocation-specific variables
5451

5552
private String geolocationOrigin;
5653
private GeolocationPermissions.Callback geolocationCallback;
5754

5855
public CustomCheckoutListener(Context context, ReactApplicationContext reactContext,
59-
@Nullable Callback onClose, @Nullable Callback onFail,
60-
@Nullable Callback onGeolocationRequest) {
56+
@Nullable Callback dispatch) {
6157
this.reactContext = reactContext;
62-
this.onCloseCallback = onClose;
63-
this.onFailCallback = onFail;
64-
this.onGeolocationRequestCallback = onGeolocationRequest;
58+
this.dispatchCallback = dispatch;
6559
}
6660

6761
// Public methods
@@ -77,35 +71,29 @@ public void invokeGeolocationCallback(boolean allow) {
7771
// Lifecycle events
7872

7973
/**
80-
* This method is called when the checkout sheet webpage requests geolocation
81-
* permissions.
82-
*
83-
* Since the app needs to request permissions first before granting, we store
84-
* the callback and origin in memory and emit a "geolocationRequest" event to
85-
* the app. The app will then request the necessary geolocation permissions
86-
* and invoke the native callback with the result.
74+
* Called when the checkout sheet's webpage requests geolocation
75+
* permissions. The platform callback is stored in memory; the dispatcher
76+
* is invoked with a `geolocationRequest` envelope so JS can either route
77+
* to a per-call handler or run the default permission flow.
8778
*
88-
* @param origin - The origin of the request
89-
* @param callback - The callback to invoke when the app requests permissions
79+
* Multi-shot — the same checkout sheet may request geolocation multiple
80+
* times during a single `present()` call, so the dispatcher is not
81+
* nulled after invocation.
9082
*/
9183
@Override
9284
public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
9385
@NonNull GeolocationPermissions.Callback callback) {
9486

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

90+
if (dispatchCallback == null) {
91+
return;
92+
}
10093
try {
101-
Map<String, Object> event = new HashMap<>();
102-
event.put("origin", origin);
103-
String payload = mapper.writeValueAsString(event);
104-
if (onGeolocationRequestCallback != null) {
105-
onGeolocationRequestCallback.invoke(payload);
106-
} else {
107-
sendEventWithStringData("geolocationRequest", payload);
108-
}
94+
Map<String, Object> payload = new HashMap<>();
95+
payload.put("origin", origin);
96+
dispatchCallback.invoke(buildEnvelope("geolocationRequest", payload));
10997
} catch (IOException e) {
11098
Log.e("ShopifyCheckoutKit", "Error emitting \"geolocationRequest\" event", e);
11199
}
@@ -115,37 +103,49 @@ public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
115103
public void onGeolocationPermissionsHidePrompt() {
116104
super.onGeolocationPermissionsHidePrompt();
117105

118-
// Reset the geolocation callback and origin when the prompt is hidden.
119106
this.geolocationCallback = null;
120107
this.geolocationOrigin = null;
121108
}
122109

123110
@Override
124111
public void onCheckoutFailed(CheckoutException checkoutError) {
125-
if (onFailCallback == null) {
112+
if (dispatchCallback == null) {
126113
return;
127114
}
128115
try {
129-
String data = mapper.writeValueAsString(populateErrorDetails(checkoutError));
130-
onFailCallback.invoke(data);
116+
dispatchCallback.invoke(buildEnvelope("fail", populateErrorDetails(checkoutError)));
131117
} catch (IOException e) {
132118
Log.e("ShopifyCheckoutKit", "Error processing checkout failed event", e);
133119
} finally {
134-
onFailCallback = null;
120+
dispatchCallback = null;
135121
}
136122
}
137123

138124
@Override
139125
public void onCheckoutCanceled() {
140-
if (onCloseCallback == null) {
126+
if (dispatchCallback == null) {
141127
return;
142128
}
143-
onCloseCallback.invoke();
144-
onCloseCallback = null;
129+
try {
130+
dispatchCallback.invoke(buildEnvelope("close", null));
131+
} catch (IOException e) {
132+
Log.e("ShopifyCheckoutKit", "Error processing checkout canceled event", e);
133+
} finally {
134+
dispatchCallback = null;
135+
}
145136
}
146137

147138
// Private
148139

140+
private String buildEnvelope(String type, @Nullable Object payload) throws IOException {
141+
ObjectNode envelope = mapper.createObjectNode();
142+
envelope.put("type", type);
143+
if (payload != null) {
144+
envelope.set("payload", mapper.valueToTree(payload));
145+
}
146+
return mapper.writeValueAsString(envelope);
147+
}
148+
149149
private Map<String, Object> populateErrorDetails(CheckoutException checkoutError) {
150150
Map<String, Object> errorMap = new HashMap();
151151
errorMap.put("__typename", getErrorTypeName(checkoutError));

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,10 @@ public void removeListeners(double count) {
8282
}
8383

8484
@ReactMethod
85-
public void present(String checkoutURL, @Nullable Callback onClose, @Nullable Callback onFail,
86-
@Nullable Callback onGeolocationRequest) {
85+
public void present(String checkoutURL, @Nullable Callback dispatch) {
8786
Activity currentActivity = getCurrentActivity();
8887
if (currentActivity instanceof ComponentActivity) {
89-
checkoutListener = new CustomCheckoutListener(currentActivity, this.reactContext, onClose,
90-
onFail, onGeolocationRequest);
88+
checkoutListener = new CustomCheckoutListener(currentActivity, this.reactContext, dispatch);
9189
currentActivity.runOnUiThread(() -> {
9290
checkoutSheet = ShopifyCheckoutKit.present(checkoutURL, (ComponentActivity) currentActivity,
9391
checkoutListener);

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ @interface RCT_EXTERN_MODULE (RCTShopifyCheckoutKit, NativeShopifyCheckoutKitSpe
4141
RCT_EXTERN_METHOD(setConfig:(NSDictionary *)configuration)
4242

4343
RCT_EXTERN_METHOD(present:(NSString *)checkoutURL
44-
onClose:(RCTResponseSenderBlock)onClose
45-
onFail:(RCTResponseSenderBlock)onFail
46-
onGeolocationRequest:(RCTResponseSenderBlock)onGeolocationRequest)
44+
dispatch:(RCTResponseSenderBlock)dispatch)
4745

4846
@end
4947

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

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,10 @@ class RCTShopifyCheckoutKit: NSObject {
3535
private var acceleratedCheckoutsApplePayConfiguration: Any?
3636
private var defaultLogLevel: LogLevel = .error
3737

38-
// TODO: invoke these once the iOS CheckoutDelegate (or equivalent) lands upstream — until then,
39-
// onClose/onFail callbacks are stored but never fire (Android is the only platform delivering them).
40-
// `pendingGeolocationRequestCallback` is intentionally a no-op on iOS — geolocation permission
41-
// is handled natively, so the callback is stored only to keep the bridge signature symmetric
42-
// with Android.
43-
private var pendingCloseCallback: RCTResponseSenderBlock?
44-
private var pendingFailCallback: RCTResponseSenderBlock?
45-
private var pendingGeolocationRequestCallback: RCTResponseSenderBlock?
38+
// TODO: invoke once the iOS CheckoutDelegate (or equivalent) lands upstream — until then,
39+
// the dispatcher is stored but never fired (Android is the only platform delivering events).
40+
// When wired, dispatch envelope JSON strings of the shape `{"type":"close"|"fail","payload":...}`.
41+
private var pendingDispatchCallback: RCTResponseSenderBlock?
4642

4743
@objc var methodQueue: DispatchQueue {
4844
return DispatchQueue.main
@@ -106,11 +102,8 @@ class RCTShopifyCheckoutKit: NSObject {
106102
invalidate()
107103
}
108104

109-
@objc func present(_ checkoutURL: String, onClose: RCTResponseSenderBlock?, onFail: RCTResponseSenderBlock?,
110-
onGeolocationRequest: RCTResponseSenderBlock?) {
111-
pendingCloseCallback = onClose
112-
pendingFailCallback = onFail
113-
pendingGeolocationRequestCallback = onGeolocationRequest
105+
@objc func present(_ checkoutURL: String, dispatch: RCTResponseSenderBlock?) {
106+
pendingDispatchCallback = dispatch
114107

115108
DispatchQueue.main.async {
116109
if let url = URL(string: checkoutURL), let viewController = self.getCurrentViewController() {

0 commit comments

Comments
 (0)