Skip to content
Closed
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
3 changes: 3 additions & 0 deletions platforms/react-native/__mocks__/react-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ const ShopifyCheckoutKit = {
onDispatch: jest.fn((callback: (envelopeJson: string) => void) =>
shopifyCheckoutKitEventEmitter.addListener('onDispatch', callback),
),
onMessageRejected: jest.fn(callback =>
shopifyCheckoutKitEventEmitter.addListener('onMessageRejected', callback),
),
preload: jest.fn(),
present: jest.fn(),
dismiss: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import kotlin.Unit;

public class ShopifyCheckoutKitModule extends NativeShopifyCheckoutKitSpec {

Expand Down Expand Up @@ -62,7 +65,7 @@
public void present(String checkoutURL, ReadableArray subscribedMethods) {
releaseCheckoutListener();

Activity currentActivity = getCurrentActivity();

Check warning on line 68 in platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

View workflow job for this annotation

GitHub Actions / React Native / Run Android Tests

[removal] getCurrentActivity() in ReactContextBaseJavaModule has been deprecated and marked for removal

Check warning on line 68 in platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

View workflow job for this annotation

GitHub Actions / React Native / Run Android Tests

[removal] getCurrentActivity() in ReactContextBaseJavaModule has been deprecated and marked for removal

Check warning on line 68 in platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

View workflow job for this annotation

GitHub Actions / React Native / Build Android Sample

[removal] getCurrentActivity() in ReactContextBaseJavaModule has been deprecated and marked for removal
if (currentActivity instanceof ComponentActivity) {
DispatchHandle dispatch = new DispatchHandle(json -> emitOnDispatch(json));
CustomCheckoutListener listener = new CustomCheckoutListener(dispatch);
Expand Down Expand Up @@ -99,7 +102,7 @@

@ReactMethod
public void preload(String checkoutURL) {
Activity currentActivity = getCurrentActivity();

Check warning on line 105 in platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

View workflow job for this annotation

GitHub Actions / React Native / Run Android Tests

[removal] getCurrentActivity() in ReactContextBaseJavaModule has been deprecated and marked for removal

Check warning on line 105 in platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

View workflow job for this annotation

GitHub Actions / React Native / Run Android Tests

[removal] getCurrentActivity() in ReactContextBaseJavaModule has been deprecated and marked for removal

Check warning on line 105 in platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java

View workflow job for this annotation

GitHub Actions / React Native / Build Android Sample

[removal] getCurrentActivity() in ReactContextBaseJavaModule has been deprecated and marked for removal
if (currentActivity instanceof ComponentActivity) {
ShopifyCheckoutKit.preload(checkoutURL, (ComponentActivity) currentActivity);
}
Expand All @@ -125,6 +128,8 @@
resultConfig.putString("colorScheme", colorSchemeStringFor(checkoutConfig.getAppearance()));
resultConfig.putString("logLevel", logLevelStringFor(checkoutConfig.getLogLevel()));
resultConfig.putBoolean("preloading", checkoutConfig.getPreloading().getEnabled());
resultConfig.putArray("allowedMessageOrigins",
Arguments.fromList(new ArrayList<>(checkoutConfig.getAllowedMessageOrigins())));

return resultConfig;
}
Expand All @@ -140,6 +145,24 @@
configuration.setPreloading(new Preloading(config.getBoolean("preloading")));
}

if (config.hasKey("allowedMessageOrigins")) {
configuration.setAllowedMessageOrigins(toStringSet(config.getArray("allowedMessageOrigins")));
}

if (config.hasKey("hasMessageRejectedCallback")
&& config.getBoolean("hasMessageRejectedCallback")) {
configuration.setOnMessageRejected(rejection -> {
WritableMap detail = Arguments.createMap();
detail.putString("origin", rejection.getOrigin());
detail.putString("message", rejection.getMessage());
detail.putString("reason", rejection.getReason());
emitOnMessageRejected(detail);
return Unit.INSTANCE;
});
} else {
configuration.setOnMessageRejected(null);
}

if (config.hasKey("logLevel")) {
LogLevel logLevel = logLevelFor(config.getString("logLevel"));

Expand Down Expand Up @@ -275,6 +298,20 @@
return STOREFRONT_COLOR_SCHEME;
}

private static Set<String> toStringSet(ReadableArray array) {
Set<String> values = new HashSet<>();
if (array == null) {
return values;
}
for (int i = 0; i < array.size(); i++) {
String value = array.getString(i);
if (value != null) {
values.add(value);
}
}
return values;
}

static LogLevel logLevelFor(String logLevel) {
if (logLevel == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,15 @@ export enum ColorScheme {
storefront = "storefront"
}

// Warning: (ae-forgotten-export) The symbol "CommonConfiguration" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export interface CommonConfiguration {
allowedMessageOrigins?: string[];
logLevel?: LogLevel;
onMessageRejected?: (detail: RejectedMessage) => void;
preloading?: boolean;
title?: string;
}

// @public (undocumented)
export type Configuration = CommonConfiguration & {
acceleratedCheckouts?: AcceleratedCheckoutConfiguration;
Expand Down Expand Up @@ -274,6 +281,16 @@ export interface PresentCallbacks {
// @public (undocumented)
export type ProtocolHandlers = ProtocolHandlers_2<CheckoutProtocolPayloads>;

// @public (undocumented)
export interface RejectedMessage {
// (undocumented)
message: string;
// (undocumented)
origin: string;
// (undocumented)
reason: string;
}

// @public (undocumented)
export enum RenderState {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ - (void)emitOnDispatchFromSwift:(NSString *)value
eventEmitterCallbackWrapper->_eventEmitterCallback("onDispatch", value);
}

- (void)emitOnMessageRejectedFromSwift:(NSDictionary *)value
{
EventEmitterCallbackWrapper *eventEmitterCallbackWrapper =
(EventEmitterCallbackWrapper *)objc_getAssociatedObject(
self, RCTShopifyCheckoutKitEventEmitterCallbackKey);

if (eventEmitterCallbackWrapper == nil) {
return;
}

eventEmitterCallbackWrapper->_eventEmitterCallback("onMessageRejected", value);
}

@end

// TurboModule registration. `RCTModuleProviders` (generated by codegen from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,24 @@ class RCTShopifyCheckoutKit: NSObject {
ShopifyCheckoutKit.configuration.preloading.enabled = preloading
}

if let allowedMessageOrigins = configuration["allowedMessageOrigins"] as? [String] {
ShopifyCheckoutKit.configuration.allowedMessageOrigins = allowedMessageOrigins
}

if let colorScheme = configuration["colorScheme"] as? String,
let appearance = appearanceFor(colorScheme)
{
ShopifyCheckoutKit.configuration.appearance = appearance
}

if configuration["hasMessageRejectedCallback"] as? Bool == true {
ShopifyCheckoutKit.configuration.onMessageRejected = { [weak self] rejection in
self?.emitMessageRejected(rejection)
}
} else {
ShopifyCheckoutKit.configuration.onMessageRejected = nil
}

if let tintColorHex = iosConfig?["tintColor"] as? String {
ShopifyCheckoutKit.configuration.tintColor = UIColor(hex: tintColorHex)
}
Expand Down Expand Up @@ -199,6 +211,7 @@ class RCTShopifyCheckoutKit: NSObject {
"tintColor": ShopifyCheckoutKit.configuration.tintColor,
"backgroundColor": ShopifyCheckoutKit.configuration.backgroundColor,
"closeButtonColor": ShopifyCheckoutKit.configuration.closeButtonTintColor,
"allowedMessageOrigins": ShopifyCheckoutKit.configuration.allowedMessageOrigins,
"logLevel": logLevelToString(ShopifyCheckoutKit.configuration.logLevel)
]
}
Expand Down Expand Up @@ -343,6 +356,17 @@ extension RCTShopifyCheckoutKit {
perform(NSSelectorFromString("emitOnDispatchFromSwift:"), with: json)
}

private func emitMessageRejected(_ rejection: MessageRejection) {
perform(
NSSelectorFromString("emitOnMessageRejectedFromSwift:"),
with: [
"origin": rejection.origin,
"message": rejection.message,
"reason": rejection.reason
]
)
}

/// Builds a `{ "type": ..., "payload": ... }` envelope and forwards
/// it to the JS dispatch event stream.
private func emitDispatchEnvelope(type: DispatchEventType, payload: [String: Any]?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export function ShopifyCheckoutProvider({
instance.current = new ShopifyCheckout(configuration, features);
}

useEffect(() => {
const checkout = instance.current;
return () => checkout?.teardown();
}, []);

useEffect(() => {
if (!instance.current || !configuration) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import type {CheckoutException} from './errors';
import type {ProtocolHandlers} from './protocol';
import type {
ApplePayContactField,
ColorScheme,
LogLevel,
} from './enums';
import type {ApplePayContactField, ColorScheme, LogLevel} from './enums';
export {
AcceleratedCheckoutWallet,
ApplePayContactField,
Expand Down Expand Up @@ -81,7 +77,7 @@ export interface AndroidAutomaticColors {
dark: AndroidColors;
}

interface CommonConfiguration {
export interface CommonConfiguration {
/**
* Sets the title of the Checkout sheet.
*
Expand Down Expand Up @@ -113,6 +109,33 @@ interface CommonConfiguration {
* @default true
*/
preloading?: boolean;
/**
* Origins trusted to send incoming checkout messages, in addition to the
* loaded checkout origin and `shop.app` (including its subdomains).
*
* The native surface is open by default: when this is empty (the default),
* messages from any origin are accepted. Provide one or more origins to
* restrict which origins are trusted. Entries may be exact origins
* (`https://example.com`), wildcard subdomains (`https://*.example.com`), or
* `'*'` to explicitly disable origin validation.
*
* Rejected messages are logged by the native SDK at debug level; they are
* never silently dropped.
*
* @default [] (all origins trusted)
*/
allowedMessageOrigins?: string[];
Comment thread
tiagocandido marked this conversation as resolved.
/**
* Invoked when an incoming checkout message is rejected by origin
* validation. Treat the payload as untrusted.
*/
onMessageRejected?: (detail: RejectedMessage) => void;
}

export interface RejectedMessage {
origin: string;
message: string;
reason: string;
}

export type Configuration = CommonConfiguration & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import type {
AndroidAutomaticColors,
AndroidColors,
Configuration,
CommonConfiguration,
Features,
GeolocationRequestEvent,
IosColors,
PresentCallbacks,
RejectedMessage,
ShopifyCheckoutKit,
} from './index.d';
import {AcceleratedCheckoutWallet} from './enums';
Expand All @@ -48,6 +50,12 @@ const defaultFeatures: Features = {
handleGeolocationRequests: true,
};

// Native checkout configuration is process-global, so its rejection callback
// must also have one shared subscription and one current owner in JavaScript.
let messageRejectedSubscription: {remove: () => void} | undefined;
let messageRejectedCallback: Configuration['onMessageRejected'];
let messageRejectedOwner: ShopifyCheckout | undefined;

class ShopifyCheckout implements ShopifyCheckoutKit {
private features: Features;

Expand Down Expand Up @@ -160,7 +168,12 @@ class ShopifyCheckout implements ShopifyCheckoutKit {
* @returns The current Configuration
*/
public getConfig(): Configuration {
return coerceConfigurationResult(RNShopifyCheckoutKit.getConfig());
return {
...coerceConfigurationResult(RNShopifyCheckoutKit.getConfig()),
...(messageRejectedCallback
? {onMessageRejected: messageRejectedCallback}
: {}),
};
}

/**
Expand All @@ -173,16 +186,25 @@ class ShopifyCheckout implements ShopifyCheckoutKit {
configuration.acceleratedCheckouts,
);
}
RNShopifyCheckoutKit.setConfig(configuration);
this.configureMessageRejectionCallback(configuration.onMessageRejected);
const nativeConfiguration = {...configuration};
delete nativeConfiguration.onMessageRejected;
RNShopifyCheckoutKit.setConfig({
...nativeConfiguration,
hasMessageRejectedCallback:
typeof configuration.onMessageRejected === 'function',
});
}

/**
* Cleans up resources and event listeners used by the checkout sheet.
* Currently a no-op — retained as part of the public API for forward
* compatibility with future protocol-client subscriptions.
*/
public teardown() {
this.releaseDispatchSubscription();
if (messageRejectedOwner === this) {
this.configureMessageRejectionCallback(undefined);
RNShopifyCheckoutKit.setConfig({hasMessageRejectedCallback: false});
}
}

/**
Expand Down Expand Up @@ -233,6 +255,21 @@ class ShopifyCheckout implements ShopifyCheckoutKit {

// --- private

private configureMessageRejectionCallback(
callback: Configuration['onMessageRejected'],
): void {
messageRejectedCallback = callback;
messageRejectedOwner = callback ? this : undefined;
if (callback && !messageRejectedSubscription) {
messageRejectedSubscription = RNShopifyCheckoutKit.onMessageRejected(
detail => messageRejectedCallback?.(detail),
);
} else if (!callback && messageRejectedSubscription) {
messageRejectedSubscription.remove();
messageRejectedSubscription = undefined;
}
}

/**
* Accelerated Checkouts is only supported from iOS 16.0 onwards
*/
Expand Down Expand Up @@ -374,7 +411,6 @@ class ShopifyCheckout implements ShopifyCheckoutKit {
private permissionGranted(status: PermissionStatus): boolean {
return status === 'granted';
}

}

// API
Expand Down Expand Up @@ -408,12 +444,14 @@ export type {
CheckoutProtocolMethod,
CheckoutProtocolPayloads,
Configuration,
CommonConfiguration,
ErrorResponse,
Features,
GeolocationRequestEvent,
IosColors,
PresentCallbacks,
ProtocolHandlers,
RejectedMessage,
RenderStateChangeEvent,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type ConfigurationSpec = {
colorScheme?: string;
logLevel?: string;
preloading?: boolean;
allowedMessageOrigins?: string[];
Comment thread
tiagocandido marked this conversation as resolved.
hasMessageRejectedCallback: boolean;
colors?: ColorsSpec;
};

Expand All @@ -46,15 +48,20 @@ type ConfigurationResultSpec = {
tintColor?: string;
backgroundColor?: string;
closeButtonColor?: string;
allowedMessageOrigins: string[];
};

export type RejectedMessageSpec = {
origin: string;
message: string;
reason: string;
};

export interface Spec extends TurboModule {
readonly onDispatch: CodegenTypes.EventEmitter<string>;
readonly onMessageRejected: CodegenTypes.EventEmitter<RejectedMessageSpec>;

present(
checkoutUrl: string,
subscribedMethods: string[],
): void;
present(checkoutUrl: string, subscribedMethods: string[]): void;
preload(checkoutUrl: string): void;
dismiss(): void;
invalidateCache(): void;
Expand Down
Loading
Loading