From e01489979b278d72e97fab656f9484cd6669d4a5 Mon Sep 17 00:00:00 2001 From: Michael Xu Date: Wed, 15 Jul 2026 13:19:00 -0400 Subject: [PATCH 1/6] feat: incoming message origin validation for react native --- .../checkoutkit/ShopifyCheckoutKitModule.java | 20 +++++++++++++++++++ .../ios/ShopifyCheckoutKit.swift | 4 ++++ .../checkout-kit-react-native/src/index.d.ts | 16 +++++++++++++++ .../src/specs/NativeShopifyCheckoutKit.ts | 1 + .../tests/index.test.ts | 12 +++++++++++ 5 files changed, 53 insertions(+) diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java index 40a3354b5..92ea39048 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java @@ -13,10 +13,12 @@ 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; public class ShopifyCheckoutKitModule extends NativeShopifyCheckoutKitSpec { @@ -140,6 +142,10 @@ public void setConfig(ReadableMap config) { configuration.setPreloading(new Preloading(config.getBoolean("preloading"))); } + if (config.hasKey("allowedMessageOrigins")) { + configuration.setAllowedMessageOrigins(toStringSet(config.getArray("allowedMessageOrigins"))); + } + if (config.hasKey("logLevel")) { LogLevel logLevel = logLevelFor(config.getString("logLevel")); @@ -275,6 +281,20 @@ static String colorSchemeStringFor(CheckoutAppearance appearance) { return STOREFRONT_COLOR_SCHEME; } + private static Set toStringSet(ReadableArray array) { + Set 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; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift index 1a35c076d..7b9f0fb19 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift @@ -164,6 +164,10 @@ 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) { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts index e274b9993..b57a66e5c 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts @@ -113,6 +113,22 @@ 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[]; } export type Configuration = CommonConfiguration & { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts index c4bcaee5e..8e8d6cf93 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts @@ -35,6 +35,7 @@ type ConfigurationSpec = { colorScheme?: string; logLevel?: string; preloading?: boolean; + allowedMessageOrigins?: string[]; colors?: ColorsSpec; }; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts index fc949e42a..d142945b8 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts @@ -209,6 +209,18 @@ describe('ShopifyCheckoutKit', () => { instance.setConfig(configWithTitle); expect(NativeModule.setConfig).toHaveBeenCalledWith(configWithTitle); }); + + it('calls `setConfig` with allowedMessageOrigins configuration', () => { + const instance = new ShopifyCheckout(); + const configWithAllowedOrigins: Configuration = { + colorScheme: ColorScheme.automatic, + allowedMessageOrigins: ['https://example.com', 'https://*.example.com'], + }; + instance.setConfig(configWithAllowedOrigins); + expect(NativeModule.setConfig).toHaveBeenCalledWith( + configWithAllowedOrigins, + ); + }); }); describe('preload', () => { From d7f74baee741217c0dc7a5af244fc5a6dc226459 Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Fri, 31 Jul 2026 13:24:16 +0200 Subject: [PATCH 2/6] fix(react-native): bridge message rejection callbacks --- .../react-native/__mocks__/react-native.ts | 3 + .../checkoutkit/ShopifyCheckoutKitModule.java | 12 +++ .../api/checkout-kit-react-native.api.md | 21 ++++- .../ios/ShopifyCheckoutKit.mm | 13 ++++ .../ios/ShopifyCheckoutKit.swift | 16 ++++ .../checkout-kit-react-native/src/index.d.ts | 19 +++-- .../checkout-kit-react-native/src/index.ts | 37 ++++++++- .../src/specs/NativeShopifyCheckoutKit.ts | 13 +++- .../tests/index.test.ts | 76 +++++++++++++++++++ 9 files changed, 195 insertions(+), 15 deletions(-) diff --git a/platforms/react-native/__mocks__/react-native.ts b/platforms/react-native/__mocks__/react-native.ts index f640146f0..edb9536ae 100644 --- a/platforms/react-native/__mocks__/react-native.ts +++ b/platforms/react-native/__mocks__/react-native.ts @@ -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(), diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java index 92ea39048..93efcc956 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import kotlin.Unit; public class ShopifyCheckoutKitModule extends NativeShopifyCheckoutKitSpec { @@ -127,6 +128,8 @@ public WritableMap getConfig() { 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; } @@ -146,6 +149,15 @@ public void setConfig(ReadableMap config) { configuration.setAllowedMessageOrigins(toStringSet(config.getArray("allowedMessageOrigins"))); } + 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; + }); + if (config.hasKey("logLevel")) { LogLevel logLevel = logLevelFor(config.getString("logLevel")); diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/api/checkout-kit-react-native.api.md b/platforms/react-native/modules/@shopify/checkout-kit-react-native/api/checkout-kit-react-native.api.md index 03b527b8b..ed2276e0b 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/api/checkout-kit-react-native.api.md +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/api/checkout-kit-react-native.api.md @@ -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; @@ -274,6 +281,16 @@ export interface PresentCallbacks { // @public (undocumented) export type ProtocolHandlers = ProtocolHandlers_2; +// @public (undocumented) +export interface RejectedMessage { + // (undocumented) + message: string; + // (undocumented) + origin: string; + // (undocumented) + reason: string; +} + // @public (undocumented) export enum RenderState { // (undocumented) diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.mm b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.mm index c99cf281e..a17e9ce9e 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.mm +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.mm @@ -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 diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift index 7b9f0fb19..78530df3e 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift @@ -174,6 +174,10 @@ class RCTShopifyCheckoutKit: NSObject { ShopifyCheckoutKit.configuration.appearance = appearance } + ShopifyCheckoutKit.configuration.onMessageRejected = { [weak self] rejection in + self?.emitMessageRejected(rejection) + } + if let tintColorHex = iosConfig?["tintColor"] as? String { ShopifyCheckoutKit.configuration.tintColor = UIColor(hex: tintColorHex) } @@ -203,6 +207,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) ] } @@ -347,6 +352,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]?) { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts index b57a66e5c..a95cc9976 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.d.ts @@ -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, @@ -81,7 +77,7 @@ export interface AndroidAutomaticColors { dark: AndroidColors; } -interface CommonConfiguration { +export interface CommonConfiguration { /** * Sets the title of the Checkout sheet. * @@ -129,6 +125,17 @@ interface CommonConfiguration { * @default [] (all origins trusted) */ allowedMessageOrigins?: string[]; + /** + * 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 & { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts index 20ba43eab..ba4a17b63 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts @@ -18,10 +18,12 @@ import type { AndroidAutomaticColors, AndroidColors, Configuration, + CommonConfiguration, Features, GeolocationRequestEvent, IosColors, PresentCallbacks, + RejectedMessage, ShopifyCheckoutKit, } from './index.d'; import {AcceleratedCheckoutWallet} from './enums'; @@ -53,6 +55,10 @@ class ShopifyCheckout implements ShopifyCheckoutKit { private dispatchSubscription?: {remove: () => void}; + private messageRejectedSubscription?: {remove: () => void}; + + private onMessageRejected?: (detail: RejectedMessage) => void; + private _acceleratedCheckoutsReady = false; // TurboModule constants are immutable for the lifetime of the process — @@ -160,7 +166,12 @@ class ShopifyCheckout implements ShopifyCheckoutKit { * @returns The current Configuration */ public getConfig(): Configuration { - return coerceConfigurationResult(RNShopifyCheckoutKit.getConfig()); + return { + ...coerceConfigurationResult(RNShopifyCheckoutKit.getConfig()), + ...(this.onMessageRejected + ? {onMessageRejected: this.onMessageRejected} + : {}), + }; } /** @@ -173,7 +184,9 @@ class ShopifyCheckout implements ShopifyCheckoutKit { configuration.acceleratedCheckouts, ); } - RNShopifyCheckoutKit.setConfig(configuration); + this.configureMessageRejectionCallback(configuration.onMessageRejected); + const {onMessageRejected: _, ...nativeConfiguration} = configuration; + RNShopifyCheckoutKit.setConfig(nativeConfiguration); } /** @@ -183,6 +196,9 @@ class ShopifyCheckout implements ShopifyCheckoutKit { */ public teardown() { this.releaseDispatchSubscription(); + this.messageRejectedSubscription?.remove(); + this.messageRejectedSubscription = undefined; + this.onMessageRejected = undefined; } /** @@ -233,6 +249,20 @@ class ShopifyCheckout implements ShopifyCheckoutKit { // --- private + private configureMessageRejectionCallback( + callback: Configuration['onMessageRejected'], + ): void { + this.onMessageRejected = callback; + if (callback && !this.messageRejectedSubscription) { + this.messageRejectedSubscription = RNShopifyCheckoutKit.onMessageRejected( + detail => this.onMessageRejected?.(detail), + ); + } else if (!callback && this.messageRejectedSubscription) { + this.messageRejectedSubscription.remove(); + this.messageRejectedSubscription = undefined; + } + } + /** * Accelerated Checkouts is only supported from iOS 16.0 onwards */ @@ -374,7 +404,6 @@ class ShopifyCheckout implements ShopifyCheckoutKit { private permissionGranted(status: PermissionStatus): boolean { return status === 'granted'; } - } // API @@ -408,12 +437,14 @@ export type { CheckoutProtocolMethod, CheckoutProtocolPayloads, Configuration, + CommonConfiguration, ErrorResponse, Features, GeolocationRequestEvent, IosColors, PresentCallbacks, ProtocolHandlers, + RejectedMessage, RenderStateChangeEvent, }; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts index 8e8d6cf93..b3e771a49 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts @@ -47,15 +47,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; + readonly onMessageRejected: CodegenTypes.EventEmitter; - present( - checkoutUrl: string, - subscribedMethods: string[], - ): void; + present(checkoutUrl: string, subscribedMethods: string[]): void; preload(checkoutUrl: string): void; dismiss(): void; invalidateCache(): void; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts index d142945b8..0e3edefb4 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts @@ -141,6 +141,11 @@ describe('Exports', () => { }); type Dispatch = (envelopeJson: string) => void; +type MessageRejectedDispatch = (detail: { + origin: string; + message: string; + reason: string; +}) => void; function lastDispatch(): Dispatch { const dispatch = NativeModule.onDispatch.mock.calls[ @@ -154,6 +159,16 @@ function lastDispatch(): Dispatch { return dispatch; } +function lastMessageRejectedDispatch(): MessageRejectedDispatch { + const dispatch = NativeModule.onMessageRejected.mock.calls[ + NativeModule.onMessageRejected.mock.calls.length - 1 + ]?.[0] as MessageRejectedDispatch | undefined; + if (!dispatch) { + throw new Error('Expected a message rejection event subscription'); + } + return dispatch; +} + describe('ShopifyCheckoutKit', () => { afterEach(() => { NativeModule.setConfig.mockReset(); @@ -221,6 +236,48 @@ describe('ShopifyCheckoutKit', () => { configWithAllowedOrigins, ); }); + + it('keeps onMessageRejected in JS and forwards rejection details', () => { + const first = jest.fn(); + const second = jest.fn(); + const instance = new ShopifyCheckout({onMessageRejected: first}); + const dispatch = lastMessageRejectedDispatch(); + const detail = { + origin: 'https://untrusted.example', + message: '{"type":"test"}', + reason: 'Origin is not allowed', + }; + + expect(NativeModule.setConfig).toHaveBeenCalledWith({}); + dispatch(detail); + expect(first).toHaveBeenCalledWith(detail); + + instance.setConfig({onMessageRejected: second}); + expect(NativeModule.onMessageRejected).toHaveBeenCalledTimes(1); + dispatch(detail); + expect(first).toHaveBeenCalledTimes(1); + expect(second).toHaveBeenCalledWith(detail); + }); + + it('removes the message rejection subscription when the callback is cleared', () => { + const remove = jest.fn(); + NativeModule.onMessageRejected.mockReturnValueOnce({remove}); + const instance = new ShopifyCheckout({onMessageRejected: jest.fn()}); + + instance.setConfig({}); + + expect(remove).toHaveBeenCalledTimes(1); + }); + + it('removes the message rejection subscription during teardown', () => { + const remove = jest.fn(); + NativeModule.onMessageRejected.mockReturnValueOnce({remove}); + const instance = new ShopifyCheckout({onMessageRejected: jest.fn()}); + + instance.teardown(); + + expect(remove).toHaveBeenCalledTimes(1); + }); }); describe('preload', () => { @@ -680,6 +737,25 @@ describe('ShopifyCheckoutKit', () => { expect(result.logLevel).toBe('trace'); expect(result.colorScheme).toBe('sepia'); }); + + it('returns allowed origins and the configured rejection callback', () => { + const onMessageRejected = jest.fn(); + NativeModule.getConfig.mockReturnValueOnce({ + colorScheme: 'automatic', + logLevel: 'error', + preloading: true, + allowedMessageOrigins: ['https://example.com'], + }); + const instance = new ShopifyCheckout({onMessageRejected}); + + expect(instance.getConfig()).toStrictEqual({ + colorScheme: ColorScheme.automatic, + logLevel: LogLevel.error, + preloading: true, + allowedMessageOrigins: ['https://example.com'], + onMessageRejected, + }); + }); }); describe('Geolocation', () => { From 4455f100e7b747e947f52522af264176c59ab5b3 Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Fri, 31 Jul 2026 15:38:56 +0200 Subject: [PATCH 3/6] fix(react-native): align rejection callback lifecycle --- .../checkoutkit/ShopifyCheckoutKitModule.java | 21 +++++++++------ .../ios/ShopifyCheckoutKit.swift | 8 ++++-- .../checkout-kit-react-native/src/context.tsx | 5 ++++ .../checkout-kit-react-native/src/index.ts | 11 +++++--- .../src/specs/NativeShopifyCheckoutKit.ts | 1 + .../tests/context.test.tsx | 26 +++++++++++++++++-- .../tests/index.test.ts | 26 ++++++++++++++----- .../ShopifyCheckoutKitModuleTest.java | 17 ++++++++++++ .../ShopifyCheckoutKitTests.swift | 12 +++++++++ 9 files changed, 105 insertions(+), 22 deletions(-) diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java index 93efcc956..bf00a6952 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/src/main/java/com/shopify/reactnative/checkoutkit/ShopifyCheckoutKitModule.java @@ -149,14 +149,19 @@ public void setConfig(ReadableMap config) { configuration.setAllowedMessageOrigins(toStringSet(config.getArray("allowedMessageOrigins"))); } - 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; - }); + 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")); diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift index 78530df3e..2841f0b71 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/ShopifyCheckoutKit.swift @@ -174,8 +174,12 @@ class RCTShopifyCheckoutKit: NSObject { ShopifyCheckoutKit.configuration.appearance = appearance } - ShopifyCheckoutKit.configuration.onMessageRejected = { [weak self] rejection in - self?.emitMessageRejected(rejection) + 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 { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/context.tsx b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/context.tsx index ba370ced5..f04b89b20 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/context.tsx +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/context.tsx @@ -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; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts index ba4a17b63..54989d672 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts @@ -185,14 +185,17 @@ class ShopifyCheckout implements ShopifyCheckoutKit { ); } this.configureMessageRejectionCallback(configuration.onMessageRejected); - const {onMessageRejected: _, ...nativeConfiguration} = configuration; - RNShopifyCheckoutKit.setConfig(nativeConfiguration); + 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(); diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts index b3e771a49..745beb2e7 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/NativeShopifyCheckoutKit.ts @@ -36,6 +36,7 @@ type ConfigurationSpec = { logLevel?: string; preloading?: boolean; allowedMessageOrigins?: string[]; + hasMessageRejectedCallback: boolean; colors?: ColorsSpec; }; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx index bb8195e4b..c91faf94b 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx @@ -52,6 +52,25 @@ describe('ShopifyCheckoutProvider', () => { expect(component).toBeTruthy(); }); + it('removes the message rejection subscription on unmount', () => { + const remove = jest.fn(); + NativeModules.ShopifyCheckoutKit.onMessageRejected.mockReturnValueOnce({ + remove, + }); + const configuration: Configuration = { + onMessageRejected: jest.fn(), + }; + + const component = render( + + + , + ); + component.unmount(); + + expect(remove).toHaveBeenCalledTimes(1); + }); + it('creates ShopifyCheckout instance with configuration', () => { render( @@ -61,7 +80,7 @@ describe('ShopifyCheckoutProvider', () => { expect( NativeModules.ShopifyCheckoutKit.setConfig, - ).toHaveBeenCalledWith(config); + ).toHaveBeenCalledWith({...config, hasMessageRejectedCallback: false}); }); it('skips configuration when no configuration is provided', () => { @@ -357,7 +376,10 @@ describe('useShopifyCheckout', () => { expect( NativeModules.ShopifyCheckoutKit.setConfig, - ).toHaveBeenCalledWith(newConfig); + ).toHaveBeenCalledWith({ + ...newConfig, + hasMessageRejectedCallback: false, + }); }); it('provides getConfig function', async () => { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts index 0e3edefb4..9da831f54 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts @@ -178,7 +178,10 @@ describe('ShopifyCheckoutKit', () => { describe('instantiation', () => { it('calls `setConfig` with the specified config on instantiation', () => { new ShopifyCheckout(config); - expect(NativeModule.setConfig).toHaveBeenCalledWith(config); + expect(NativeModule.setConfig).toHaveBeenCalledWith({ + ...config, + hasMessageRejectedCallback: false, + }); }); it('does not call `setConfig` if no config was specified on instantiation', () => { @@ -192,7 +195,10 @@ describe('ShopifyCheckoutKit', () => { const instance = new ShopifyCheckout(); instance.setConfig(config); expect(NativeModule.setConfig).toHaveBeenCalledTimes(1); - expect(NativeModule.setConfig).toHaveBeenCalledWith(config); + expect(NativeModule.setConfig).toHaveBeenCalledWith({ + ...config, + hasMessageRejectedCallback: false, + }); }); it('calls `setConfig` with logLevel configuration', () => { @@ -202,7 +208,10 @@ describe('ShopifyCheckoutKit', () => { logLevel: LogLevel.debug, }; instance.setConfig(configWithLogLevel); - expect(NativeModule.setConfig).toHaveBeenCalledWith(configWithLogLevel); + expect(NativeModule.setConfig).toHaveBeenCalledWith({ + ...configWithLogLevel, + hasMessageRejectedCallback: false, + }); }); it('calls `setConfig` with preloading configuration', () => { @@ -212,7 +221,10 @@ describe('ShopifyCheckoutKit', () => { preloading: false, }; instance.setConfig(configWithPreloading); - expect(NativeModule.setConfig).toHaveBeenCalledWith(configWithPreloading); + expect(NativeModule.setConfig).toHaveBeenCalledWith({ + ...configWithPreloading, + hasMessageRejectedCallback: false, + }); }); it('calls `setConfig` with title configuration', () => { @@ -233,7 +245,7 @@ describe('ShopifyCheckoutKit', () => { }; instance.setConfig(configWithAllowedOrigins); expect(NativeModule.setConfig).toHaveBeenCalledWith( - configWithAllowedOrigins, + {...configWithAllowedOrigins, hasMessageRejectedCallback: false}, ); }); @@ -248,7 +260,9 @@ describe('ShopifyCheckoutKit', () => { reason: 'Origin is not allowed', }; - expect(NativeModule.setConfig).toHaveBeenCalledWith({}); + expect(NativeModule.setConfig).toHaveBeenCalledWith({ + hasMessageRejectedCallback: true, + }); dispatch(detail); expect(first).toHaveBeenCalledWith(detail); diff --git a/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java b/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java index 1a66e8940..e2620d907 100644 --- a/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java +++ b/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java @@ -317,6 +317,23 @@ public void testUnknownColorSchemeKeepsTheNativeDefaultAppearance() { .isEqualTo("storefront"); } + @Test + public void testOnlyInstallsMessageRejectedCallbackWhenRequested() { + JavaOnlyMap config = new JavaOnlyMap(); + config.putArray("allowedMessageOrigins", JavaOnlyArray.from(List.of("https://example.com"))); + + shopifyCheckoutKitModule.setConfig(config); + assertThat(ShopifyCheckoutKitModule.checkoutConfig.getOnMessageRejected()).isNull(); + + config.putBoolean("hasMessageRejectedCallback", true); + shopifyCheckoutKitModule.setConfig(config); + assertThat(ShopifyCheckoutKitModule.checkoutConfig.getOnMessageRejected()).isNotNull(); + + config.putBoolean("hasMessageRejectedCallback", false); + shopifyCheckoutKitModule.setConfig(config); + assertThat(ShopifyCheckoutKitModule.checkoutConfig.getOnMessageRejected()).isNull(); + } + @Test public void testCanConfigureLightColorSchemeWithValidColors() { JavaOnlyMap androidColors = createValidLightColors(); diff --git a/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/ShopifyCheckoutKitTests.swift b/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/ShopifyCheckoutKitTests.swift index 027e8c8d9..0b1512026 100644 --- a/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/ShopifyCheckoutKitTests.swift +++ b/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/ShopifyCheckoutKitTests.swift @@ -22,6 +22,7 @@ class ShopifyCheckoutKitTests: XCTestCase { ShopifyCheckoutKit.configuration.closeButtonTintColor = nil ShopifyCheckoutKit.configuration.logLevel = LogLevel.warn ShopifyCheckoutKit.configuration.preloading.enabled = true + ShopifyCheckoutKit.configuration.onMessageRejected = nil } private func getShopifyCheckoutKit() -> RCTShopifyCheckoutKit { @@ -99,6 +100,17 @@ class ShopifyCheckoutKitTests: XCTestCase { XCTAssertEqual(result?["title"] as? String, "Custom Checkout") } + func testOnlyInstallsMessageRejectedCallbackWhenRequested() { + shopifyCheckoutKit.setConfig(["allowedMessageOrigins": ["https://example.com"]]) + XCTAssertNil(ShopifyCheckoutKit.configuration.onMessageRejected) + + shopifyCheckoutKit.setConfig(["hasMessageRejectedCallback": true]) + XCTAssertNotNil(ShopifyCheckoutKit.configuration.onMessageRejected) + + shopifyCheckoutKit.setConfig(["hasMessageRejectedCallback": false]) + XCTAssertNil(ShopifyCheckoutKit.configuration.onMessageRejected) + } + func testConfigureWithInvalidColors() { let configuration: [AnyHashable: Any] = [ "colors": [ From bf5b908209492e7c61eaac909a91c63787f647b6 Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Mon, 3 Aug 2026 13:53:31 +0200 Subject: [PATCH 4/6] test(react-native): build allowed origins explicitly --- .../reactnativedemo/ShopifyCheckoutKitModuleTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java b/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java index e2620d907..0baf70494 100644 --- a/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java +++ b/platforms/react-native/sample/android/app/src/test/java/com/shopify/checkoutkit/reactnativedemo/ShopifyCheckoutKitModuleTest.java @@ -320,7 +320,9 @@ public void testUnknownColorSchemeKeepsTheNativeDefaultAppearance() { @Test public void testOnlyInstallsMessageRejectedCallbackWhenRequested() { JavaOnlyMap config = new JavaOnlyMap(); - config.putArray("allowedMessageOrigins", JavaOnlyArray.from(List.of("https://example.com"))); + JavaOnlyArray allowedMessageOrigins = new JavaOnlyArray(); + allowedMessageOrigins.pushString("https://example.com"); + config.putArray("allowedMessageOrigins", allowedMessageOrigins); shopifyCheckoutKitModule.setConfig(config); assertThat(ShopifyCheckoutKitModule.checkoutConfig.getOnMessageRejected()).isNull(); From 4b14f6c1f5a80fbfa0083c9f979dcfc6aff496fc Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Wed, 12 Aug 2026 12:31:32 +0200 Subject: [PATCH 5/6] test(react-native): include callback flag in title config --- .../@shopify/checkout-kit-react-native/tests/index.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts index 9da831f54..656143a0d 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts @@ -234,7 +234,10 @@ describe('ShopifyCheckoutKit', () => { title: 'Custom Checkout', }; instance.setConfig(configWithTitle); - expect(NativeModule.setConfig).toHaveBeenCalledWith(configWithTitle); + expect(NativeModule.setConfig).toHaveBeenCalledWith({ + ...configWithTitle, + hasMessageRejectedCallback: false, + }); }); it('calls `setConfig` with allowedMessageOrigins configuration', () => { From a5f0480ec75643666ede5f73a8b482ebd426f36b Mon Sep 17 00:00:00 2001 From: tiagocandido Date: Wed, 12 Aug 2026 14:38:37 +0200 Subject: [PATCH 6/6] fix(react-native): align rejection callback ownership --- .../checkout-kit-react-native/src/index.ts | 36 ++++++++++--------- .../tests/context.test.tsx | 3 ++ .../tests/index.test.ts | 36 +++++++++++++++++++ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts index 54989d672..38c885b3f 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/index.ts @@ -50,15 +50,17 @@ 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; private dispatchSubscription?: {remove: () => void}; - private messageRejectedSubscription?: {remove: () => void}; - - private onMessageRejected?: (detail: RejectedMessage) => void; - private _acceleratedCheckoutsReady = false; // TurboModule constants are immutable for the lifetime of the process — @@ -168,8 +170,8 @@ class ShopifyCheckout implements ShopifyCheckoutKit { public getConfig(): Configuration { return { ...coerceConfigurationResult(RNShopifyCheckoutKit.getConfig()), - ...(this.onMessageRejected - ? {onMessageRejected: this.onMessageRejected} + ...(messageRejectedCallback + ? {onMessageRejected: messageRejectedCallback} : {}), }; } @@ -199,9 +201,10 @@ class ShopifyCheckout implements ShopifyCheckoutKit { */ public teardown() { this.releaseDispatchSubscription(); - this.messageRejectedSubscription?.remove(); - this.messageRejectedSubscription = undefined; - this.onMessageRejected = undefined; + if (messageRejectedOwner === this) { + this.configureMessageRejectionCallback(undefined); + RNShopifyCheckoutKit.setConfig({hasMessageRejectedCallback: false}); + } } /** @@ -255,14 +258,15 @@ class ShopifyCheckout implements ShopifyCheckoutKit { private configureMessageRejectionCallback( callback: Configuration['onMessageRejected'], ): void { - this.onMessageRejected = callback; - if (callback && !this.messageRejectedSubscription) { - this.messageRejectedSubscription = RNShopifyCheckoutKit.onMessageRejected( - detail => this.onMessageRejected?.(detail), + messageRejectedCallback = callback; + messageRejectedOwner = callback ? this : undefined; + if (callback && !messageRejectedSubscription) { + messageRejectedSubscription = RNShopifyCheckoutKit.onMessageRejected( + detail => messageRejectedCallback?.(detail), ); - } else if (!callback && this.messageRejectedSubscription) { - this.messageRejectedSubscription.remove(); - this.messageRejectedSubscription = undefined; + } else if (!callback && messageRejectedSubscription) { + messageRejectedSubscription.remove(); + messageRejectedSubscription = undefined; } } diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx index c91faf94b..4ca8f86b3 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/context.test.tsx @@ -69,6 +69,9 @@ describe('ShopifyCheckoutProvider', () => { component.unmount(); expect(remove).toHaveBeenCalledTimes(1); + expect( + NativeModules.ShopifyCheckoutKit.setConfig, + ).toHaveBeenLastCalledWith({hasMessageRejectedCallback: false}); }); it('creates ShopifyCheckout instance with configuration', () => { diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts index 656143a0d..6041d1cf2 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/index.test.ts @@ -274,6 +274,37 @@ describe('ShopifyCheckoutKit', () => { dispatch(detail); expect(first).toHaveBeenCalledTimes(1); expect(second).toHaveBeenCalledWith(detail); + + instance.teardown(); + }); + + it('keeps a single global callback owned by the latest configured instance', () => { + const remove = jest.fn(); + NativeModule.onMessageRejected.mockReturnValueOnce({remove}); + const firstCallback = jest.fn(); + const secondCallback = jest.fn(); + const first = new ShopifyCheckout({onMessageRejected: firstCallback}); + const dispatch = lastMessageRejectedDispatch(); + const second = new ShopifyCheckout({onMessageRejected: secondCallback}); + const detail = { + origin: 'https://untrusted.example', + message: '{"type":"test"}', + reason: 'Origin is not allowed', + }; + + expect(NativeModule.onMessageRejected).toHaveBeenCalledTimes(1); + dispatch(detail); + expect(firstCallback).not.toHaveBeenCalled(); + expect(secondCallback).toHaveBeenCalledWith(detail); + + first.teardown(); + expect(remove).not.toHaveBeenCalled(); + + second.teardown(); + expect(remove).toHaveBeenCalledTimes(1); + expect(NativeModule.setConfig).toHaveBeenLastCalledWith({ + hasMessageRejectedCallback: false, + }); }); it('removes the message rejection subscription when the callback is cleared', () => { @@ -294,6 +325,9 @@ describe('ShopifyCheckoutKit', () => { instance.teardown(); expect(remove).toHaveBeenCalledTimes(1); + expect(NativeModule.setConfig).toHaveBeenLastCalledWith({ + hasMessageRejectedCallback: false, + }); }); }); @@ -772,6 +806,8 @@ describe('ShopifyCheckoutKit', () => { allowedMessageOrigins: ['https://example.com'], onMessageRejected, }); + + instance.teardown(); }); });