Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ of this software and associated documentation files (the "Software"), to deal
import com.facebook.react.bridge.ReactApplicationContext;
import com.shopify.checkoutkit.lifecycleevents.CheckoutCompletedEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -45,24 +46,17 @@ public class CustomCheckoutEventProcessor extends DefaultCheckoutEventProcessor
private final ObjectMapper mapper = new ObjectMapper();

@Nullable
private Callback onCloseCallback;
@Nullable
private Callback onFailCallback;
@Nullable
private Callback onGeolocationRequestCallback;
private Callback dispatchCallback;

// Geolocation-specific variables

private String geolocationOrigin;
private GeolocationPermissions.Callback geolocationCallback;

public CustomCheckoutEventProcessor(Context context, ReactApplicationContext reactContext,
@Nullable Callback onClose, @Nullable Callback onFail,
@Nullable Callback onGeolocationRequest) {
@Nullable Callback dispatch) {
this.reactContext = reactContext;
this.onCloseCallback = onClose;
this.onFailCallback = onFail;
this.onGeolocationRequestCallback = onGeolocationRequest;
this.dispatchCallback = dispatch;
}

// Public methods
Expand All @@ -78,35 +72,29 @@ public void invokeGeolocationCallback(boolean allow) {
// 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.
* Called when the checkout sheet's webpage requests geolocation
* permissions. The platform callback is stored in memory; the dispatcher
* is invoked with a `geolocationRequest` envelope so JS can either route
* to a per-call handler or run the default permission flow.
*
* @param origin - The origin of the request
* @param callback - The callback to invoke when the app requests permissions
* Multi-shot — the same checkout sheet may request geolocation multiple
* times during a single `present()` call, so the dispatcher is not
* nulled after invocation.
*/
@Override
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;

if (dispatchCallback == null) {
return;
}
try {
Map<String, Object> event = new HashMap<>();
event.put("origin", origin);
String payload = mapper.writeValueAsString(event);
if (onGeolocationRequestCallback != null) {
onGeolocationRequestCallback.invoke(payload);
} else {
sendEventWithStringData("geolocationRequest", payload);
}
Map<String, Object> payload = new HashMap<>();
payload.put("origin", origin);
dispatchCallback.invoke(buildEnvelope("geolocationRequest", payload));
} catch (IOException e) {
Log.e("ShopifyCheckoutKit", "Error emitting \"geolocationRequest\" event", e);
}
Expand All @@ -116,33 +104,36 @@ public void onGeolocationPermissionsShowPrompt(@NonNull String origin,
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 onCheckoutFailed(CheckoutException checkoutError) {
if (onFailCallback == null) {
if (dispatchCallback == null) {
return;
}
try {
String data = mapper.writeValueAsString(populateErrorDetails(checkoutError));
onFailCallback.invoke(data);
dispatchCallback.invoke(buildEnvelope("fail", populateErrorDetails(checkoutError)));
} catch (IOException e) {
Log.e("ShopifyCheckoutKit", "Error processing checkout failed event", e);
} finally {
onFailCallback = null;
dispatchCallback = null;
}
}

@Override
public void onCheckoutCanceled() {
if (onCloseCallback == null) {
if (dispatchCallback == null) {
return;
}
onCloseCallback.invoke();
onCloseCallback = null;
try {
dispatchCallback.invoke(buildEnvelope("close", null));
} catch (IOException e) {
Log.e("ShopifyCheckoutKit", "Error processing checkout canceled event", e);
} finally {
dispatchCallback = null;
}
}

@Override
Expand All @@ -157,6 +148,15 @@ public void onCheckoutCompleted(@NonNull CheckoutCompletedEvent event) {

// Private

private String buildEnvelope(String type, @Nullable Object payload) throws IOException {
ObjectNode envelope = mapper.createObjectNode();
envelope.put("type", type);
if (payload != null) {
envelope.set("payload", mapper.valueToTree(payload));
}
return mapper.writeValueAsString(envelope);
}

private Map<String, Object> populateErrorDetails(CheckoutException checkoutError) {
Map<String, Object> errorMap = new HashMap();
errorMap.put("__typename", getErrorTypeName(checkoutError));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,10 @@ public void removeListeners(double count) {
}

@ReactMethod
public void present(String checkoutURL, @Nullable Callback onClose, @Nullable Callback onFail,
@Nullable Callback onGeolocationRequest) {
public void present(String checkoutURL, @Nullable Callback dispatch) {
Activity currentActivity = getCurrentActivity();
if (currentActivity instanceof ComponentActivity) {
checkoutEventProcessor = new CustomCheckoutEventProcessor(currentActivity, this.reactContext, onClose,
onFail, onGeolocationRequest);
checkoutEventProcessor = new CustomCheckoutEventProcessor(currentActivity, this.reactContext, dispatch);
currentActivity.runOnUiThread(() -> {
checkoutSheet = ShopifyCheckoutKit.present(checkoutURL, (ComponentActivity) currentActivity,
checkoutEventProcessor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ @interface RCT_EXTERN_MODULE (RCTShopifyCheckoutKit, NativeShopifyCheckoutKitSpe
RCT_EXTERN_METHOD(setConfig:(NSDictionary *)configuration)

RCT_EXTERN_METHOD(present:(NSString *)checkoutURL
onClose:(RCTResponseSenderBlock)onClose
onFail:(RCTResponseSenderBlock)onFail
onGeolocationRequest:(RCTResponseSenderBlock)onGeolocationRequest)
dispatch:(RCTResponseSenderBlock)dispatch)

@end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,10 @@ class RCTShopifyCheckoutKit: NSObject {
private var acceleratedCheckoutsApplePayConfiguration: Any?
private var defaultLogLevel: LogLevel = .error

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

@objc var methodQueue: DispatchQueue {
return DispatchQueue.main
Expand Down Expand Up @@ -106,11 +102,8 @@ class RCTShopifyCheckoutKit: NSObject {
invalidate()
}

@objc func present(_ checkoutURL: String, onClose: RCTResponseSenderBlock?, onFail: RCTResponseSenderBlock?,
onGeolocationRequest: RCTResponseSenderBlock?) {
pendingCloseCallback = onClose
pendingFailCallback = onFail
pendingGeolocationRequestCallback = onGeolocationRequest
@objc func present(_ checkoutURL: String, dispatch: RCTResponseSenderBlock?) {
pendingDispatchCallback = dispatch

DispatchQueue.main.async {
if let url = URL(string: checkoutURL), let viewController = self.getCurrentViewController() {
Expand Down
Loading
Loading