Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
40914ab
feat: setup android inline component
kieran-osgood-shopify Nov 12, 2025
91ab2c3
fix: ensure we can respond to address change events
kieran-osgood-shopify Nov 12, 2025
6eb56fa
chore: remove redundant address change map
kieran-osgood-shopify Nov 12, 2025
b55ea61
remove unused imports
kieran-osgood-shopify Nov 12, 2025
4319800
fix: rename CheckoutCompletedEvent -> CheckoutCompleteEvent
kieran-osgood-shopify Nov 13, 2025
e585ce7
refactor: remove deprecated method overrides
kieran-osgood-shopify Nov 13, 2025
c38175e
chore: cleaning up token and networking debugging
kieran-osgood-shopify Nov 13, 2025
f0e5ced
fix: remove android debugging code - readd timeout
kieran-osgood-shopify Nov 13, 2025
c2730a0
fix: add event handlers for CheckoutScreen
kieran-osgood-shopify Nov 13, 2025
278620a
refactor: remove unnecessary view in buynowbutton
kieran-osgood-shopify Nov 13, 2025
1323004
fix: yarn module build errors
kieran-osgood-shopify Nov 13, 2025
704cfb1
refactor(RCTCheckoutWebView): replace complicated if checks with .equ…
kieran-osgood-shopify Nov 14, 2025
4926b04
refactor: remove lint warnings on type coersion
kieran-osgood-shopify Nov 14, 2025
2684297
refactor: use shorter kotlin unit instance
kieran-osgood-shopify Nov 14, 2025
09e2522
fix: remove absolute user path
kieran-osgood-shopify Nov 26, 2025
049a571
fix: issues with inconsistent names of events
kieran-osgood-shopify Nov 27, 2025
1bd7aa0
refactor: remove unnecesasry logging and overrides
kieran-osgood-shopify Nov 27, 2025
3b9ad9d
remove classPaths from build.gradle
kieran-osgood-shopify Nov 27, 2025
fe15ad7
feat: add event handling to inline webview android
kieran-osgood-shopify Nov 27, 2025
f80904b
refactor: simplifying map construction
kieran-osgood-shopify Nov 27, 2025
d33096f
refactor: remove deprecated event emitting
kieran-osgood-shopify Nov 27, 2025
ffbb322
refactor: extract CheckoutEvent for instance based events
kieran-osgood-shopify Nov 27, 2025
73fb035
fix: webview not loading
kieran-osgood-shopify Nov 27, 2025
0664faf
cleanup events that arent ready - rename onClickLink
kieran-osgood-shopify Nov 27, 2025
0d3b83a
add nonnull annotation for getEventName
kieran-osgood-shopify Nov 27, 2025
857fa72
fix: remove onLoad from swift
kieran-osgood-shopify Nov 27, 2025
fa56ba3
rename customeventprocessor -> Sheeteventprocessor
kieran-osgood-shopify Nov 27, 2025
3c82c9a
refactor: undo changes to reuse custom event processor for inline
kieran-osgood-shopify Nov 27, 2025
0d0d0f3
fix: intermitent flakiness of layout for checkout webview inline
kieran-osgood-shopify Nov 27, 2025
51c1ff4
remove unused variable
kieran-osgood-shopify Nov 27, 2025
43464ab
fix: flatten onerror - implement onlinkclick
kieran-osgood-shopify Nov 27, 2025
9999547
format files
kieran-osgood-shopify Nov 28, 2025
2e7b283
fix: type in TAG - remove unused comments
kieran-osgood-shopify Nov 28, 2025
95b387d
fix: remove record (java16 feature)
kieran-osgood-shopify Nov 28, 2025
d22b70f
fix: ensure layout params are applied to webview
kieran-osgood-shopify Nov 28, 2025
6d88aa0
test: RctCheckoutWebViewTests
kieran-osgood-shopify Nov 28, 2025
3a7c499
refactor: improve tests with side effects of setup checks
kieran-osgood-shopify Nov 28, 2025
34599b1
fix: ts error from unused prop
kieran-osgood-shopify Nov 28, 2025
235ff69
feat: update snapshot with new classes for components
kieran-osgood-shopify Nov 28, 2025
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
@@ -0,0 +1,63 @@
/*
MIT License

Copyright 2023 - Present, Shopify Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.shopify.reactnative.checkoutsheetkit;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;

/**
* Can be used to send events back to props of instances of components
*
* private void sendEvent(String eventName, WritableMap params) {
* ReactContext reactContext = this.context.getReactApplicationContext();
* int viewId = getId();
* EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewId);
* int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
* eventDispatcher.dispatchEvent(new CheckoutEvent(surfaceId, viewId, eventName, params));
* }
**/
public class CheckoutEvent extends Event<CheckoutEvent> {
private final String eventName;
private final WritableMap payload;

public CheckoutEvent(int surfaceId, int viewId, String eventName, WritableMap payload) {
super(surfaceId, viewId);
this.eventName = eventName;
this.payload = payload;
}

@NonNull
@Override
public String getEventName() {
return eventName;
}

@Override
protected WritableMap getEventData() {
return payload != null ? payload : Arguments.createMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
/*
MIT License

Copyright 2023 - Present, Shopify Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.shopify.reactnative.checkoutsheetkit;

import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.EventDispatcher;

import com.shopify.checkoutsheetkit.Authentication;
import com.shopify.checkoutsheetkit.CheckoutException;
import com.shopify.checkoutsheetkit.DefaultCheckoutEventProcessor;
import com.shopify.checkoutsheetkit.CheckoutOptions;
import com.shopify.checkoutsheetkit.CheckoutWebView;
import com.shopify.checkoutsheetkit.CheckoutWebViewEventProcessor;
import com.shopify.checkoutsheetkit.HttpException;
import com.shopify.checkoutsheetkit.CheckoutExpiredException;
import com.shopify.checkoutsheetkit.ClientException;
import com.shopify.checkoutsheetkit.ConfigurationException;
import com.shopify.checkoutsheetkit.CheckoutSheetKitException;
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutCompleteEvent;
import com.shopify.checkoutsheetkit.lifecycleevents.CheckoutStartEvent;
import com.shopify.checkoutsheetkit.rpc.events.CheckoutAddressChangeStart;
import com.shopify.checkoutsheetkit.rpc.events.CheckoutAddressChangeStartEvent;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import kotlin.Unit;

public class RCTCheckoutWebView extends FrameLayout {
Comment thread
kieran-osgood-shopify marked this conversation as resolved.
private static final String TAG = "RCTCheckoutWebView";
private final ThemedReactContext context;
private final ObjectMapper mapper = new ObjectMapper();

private CheckoutWebView checkoutWebView;
private String checkoutUrl;
private String auth;
private boolean pendingSetup = false;
private CheckoutConfiguration lastConfiguration = null;
private final Handler mainHandler = new Handler(Looper.getMainLooper());

private record CheckoutConfiguration(String url, String authToken) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just thinking about this, I think support for records might have come after what we've defined for

sourceCompatibility
targetCompatibility

Unless the android gradle plugin supports desugaring


public RCTCheckoutWebView(ThemedReactContext context) {
super(context);
this.context = context;
}

public void setCheckoutUrl(String url) {
if (Objects.equals(url, checkoutUrl)) {
return;
}

checkoutUrl = url;

if (url == null) {
removeCheckout();
} else {
scheduleSetupIfNeeded();
}
}

public void setAuth(String authToken) {
if (Objects.equals(authToken, auth)) {
return;
}

auth = authToken;
scheduleSetupIfNeeded();
}

private void scheduleSetupIfNeeded() {
Comment thread
kieran-osgood-shopify marked this conversation as resolved.
Outdated
if (pendingSetup) {
return;
}

pendingSetup = true;
mainHandler.post(this::setup);
}

private void setup() {
pendingSetup = false;

if (checkoutUrl == null) {
removeCheckout();
return;
}

CheckoutConfiguration newConfiguration = new CheckoutConfiguration(checkoutUrl, auth);
if (newConfiguration.equals(lastConfiguration)) {
return;
}

setupCheckoutWebView(checkoutUrl, newConfiguration);
}

public void setupCheckoutWebView(String url, CheckoutConfiguration configuration) {
Log.d(TAG, "setupCheckoutWebView: Setting up new webview for URL: " + url);
removeCheckout();

// Create the CheckoutWebView with null AttributeSet
checkoutWebView = new CheckoutWebView(this.context, null);
Log.d(TAG, "setupCheckoutWebView: New CheckoutWebView created");

// Set up event processor with all required parameters
Comment thread
kieran-osgood-shopify marked this conversation as resolved.
Outdated
CheckoutWebViewEventProcessor webViewEventProcessor = getCheckoutWebViewEventProcessor();
checkoutWebView.setEventProcessor(webViewEventProcessor);

// Configure authentication if provided
CheckoutOptions options = new CheckoutOptions();
if (auth != null && !auth.isEmpty()) {
options = new CheckoutOptions(new Authentication.Token(auth));
}
checkoutWebView.loadCheckout(url, options);
checkoutWebView.notifyPresented();

LayoutParams params = new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
);
addView(checkoutWebView, params);
/// Works around a race condition where onLayout executes before setupCheckoutWebView
/// resulting in an empty view being rendered. Cannot move setup to constructor as
/// checkoutUrl is undefined until setCheckoutUrl is called by RCTCheckoutWebViewManager
/// requestLayout / invalidate were unsuccessful in remedying this
checkoutWebView.layout(0, 0, getWidth(), getHeight());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to override onLayout ot deal with size changes?

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (checkoutWebView != null) {
        checkoutWebView.layout(0, 0, right - left, bottom - top);
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my initial approach which had the race condition

I've got a comment above that hopefully explains it but the onLayout was executing before setupCheckoutWebView (because the FrameLayout is constructed before react props are passed in) meaning we dont have a view to construct yet

Before:
onLayout (dimensions zero) -> setupCheckoutWebView -> view is zero sized

After:
onLayout (dimensions zero) -> setupCheckoutWebView -> re-measure layout after adding view -> view displays

I tried using requestLayout/invalidate to get android to re-run onLayout but didn't see a subsequent execution

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, yeah I was thinking in case the layout changes in some way (orientation?) and we don't resize

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - probably worth keeping it still just incase

lastConfiguration = configuration;
}

@NonNull
private CheckoutWebViewEventProcessor getCheckoutWebViewEventProcessor() {
Comment thread
kieran-osgood-shopify marked this conversation as resolved.
Activity currentActivity = this.context.getCurrentActivity();
InlineCheckoutEventProcessor eventProcessor = new InlineCheckoutEventProcessor(currentActivity);

return new CheckoutWebViewEventProcessor(
eventProcessor,
(visible) -> Unit.INSTANCE, // toggleHeader
(error) -> Unit.INSTANCE, // closeCheckoutDialogWithError
(visibility) -> Unit.INSTANCE, // setProgressBarVisibility
(percentage) -> Unit.INSTANCE // updateProgressBarPercentage
);
}

private void removeCheckout() {
Log.d(TAG, "removeCheckout: Called, webview exists: " + (checkoutWebView != null));
if (checkoutWebView != null) {
Log.d(TAG, "removeCheckout: Destroying webview");
removeView(checkoutWebView);
checkoutWebView.destroy();
checkoutWebView = null;
}
lastConfiguration = null;
}

public void reload() {
if (checkoutWebView != null) {
checkoutWebView.reload();
} else if (checkoutUrl != null) {
// Re-setup if WebView was destroyed but URL is still set
CheckoutConfiguration configuration = new CheckoutConfiguration(checkoutUrl, auth);
setupCheckoutWebView(checkoutUrl, configuration);
}
}

public void respondToEvent(String eventId, String responseData) {
Log.d(TAG, "Responding to event: " + eventId + " with data: " + responseData);

if (checkoutWebView != null) {
// Delegate to the WebView's respondToEvent method
checkoutWebView.respondToEvent(eventId, responseData);
} else {
Log.e(TAG, "CheckoutWebView is null when trying to respond to event: " + eventId);
}
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.d(TAG, "onAttachedToWindow: View attached to window, webview exists: " + (checkoutWebView != null));
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.d(TAG, "onDetachedFromWindow: View detached from window (not destroying webview), webview exists: " + (checkoutWebView != null));
// NOTE: We do NOT destroy the webview here to prevent issues during navigation
// The webview will be properly cleaned up in the ViewManager's onDropViewInstance instead
}

private WritableMap serializeToWritableMap(Object event) {
Map<String, Object> map = mapper.convertValue(event, new TypeReference<>() {});
return Arguments.makeNativeMap(map);
}

private WritableMap buildErrorMap(CheckoutException error) {
WritableMap errorMap = Arguments.createMap();
errorMap.putString("__typename", getErrorTypeName(error));
errorMap.putString("message", error.getErrorDescription());
errorMap.putBoolean("recoverable", error.isRecoverable());
errorMap.putString("code", error.getErrorCode());
if (error instanceof HttpException) {
errorMap.putInt("statusCode", ((HttpException) error).getStatusCode());
}
return errorMap;
}

private String getErrorTypeName(CheckoutException error) {
if (error instanceof CheckoutExpiredException) {
return "CheckoutExpiredError";
} else if (error instanceof ClientException) {
return "CheckoutClientError";
} else if (error instanceof HttpException) {
return "CheckoutHTTPError";
} else if (error instanceof ConfigurationException) {
return "ConfigurationError";
} else if (error instanceof CheckoutSheetKitException) {
return "InternalError";
} else {
return "UnknownError";
}
}

private void sendEvent(String eventName, WritableMap params) {
ReactContext reactContext = this.context.getReactApplicationContext();
int viewId = getId();

EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewId);
if (eventDispatcher == null) {
Log.w(TAG, "Cannot send event '" + eventName + "': EventDispatcher not available (viewId=" + viewId + ")");
return;
}

int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
eventDispatcher.dispatchEvent(new CheckoutEvent(surfaceId, viewId, eventName, params));
}

private class InlineCheckoutEventProcessor extends DefaultCheckoutEventProcessor {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kiftio moved back to RCTCheckoutWebView having its own processor so i could remove all the changes to CustomCheckoutEventProcessor (but I renamed it to SheetCheckoutEventProcessor to distinguish them)


public InlineCheckoutEventProcessor(android.content.Context context) {
super(context);
}

@Override
public void onStart(@NonNull CheckoutStartEvent event) {
try {
WritableMap data = serializeToWritableMap(event);
sendEvent("onStart", data);
} catch (Exception e) {
Log.e(TAG, "Error processing start event", e);
}
}

@Override
public void onComplete(@NonNull CheckoutCompleteEvent event) {
try {
WritableMap data = serializeToWritableMap(event);
sendEvent("onComplete", data);
} catch (Exception e) {
Log.e(TAG, "Error processing complete event", e);
}
}

@Override
public void onFail(@NonNull CheckoutException error) {
WritableMap params = Arguments.createMap();
params.putMap("error", buildErrorMap(error));
sendEvent("onError", params);
}

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

@Override
public void onAddressChangeStart(@NonNull CheckoutAddressChangeStart event) {
try {
CheckoutAddressChangeStartEvent params = event.getParams();
Map<String, Object> eventData = new HashMap<>();

eventData.put("id", event.getId());
eventData.put("type", "addressChangeStart");
eventData.put("addressType", params.getAddressType());
eventData.put("cart", params.getCart());

sendEvent("onAddressChangeStart", serializeToWritableMap(eventData));
} catch (Exception e) {
Log.e(TAG, "Error processing address change start event", e);
}
}
}
}
Loading
Loading