From 5c69ccbe1f225edcfebbcf1cf776718b52e006e1 Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Tue, 8 Sep 2026 15:18:47 +0100 Subject: [PATCH] Rename React Native dismissal callbacks Assisted-By: devx/3f0e7f93-212d-45d8-b056-138b2da8389f --- platforms/react-native/README.md | 33 +++++--- .../api/checkout-kit-react-native.api.md | 2 +- .../ios/AcceleratedCheckoutButtons.swift | 4 +- .../ios/ShopifyCheckoutKit.mm | 4 +- .../components/AcceleratedCheckoutButtons.tsx | 14 ++-- .../checkout-kit-react-native/src/index.d.ts | 14 ++-- .../checkout-kit-react-native/src/index.ts | 6 +- .../src/present-dispatcher.ts | 2 +- ...celeratedCheckoutButtonsNativeComponent.ts | 2 +- .../tests/AcceleratedCheckoutButtons.test.tsx | 41 ++++++++-- .../tests/context.test.tsx | 4 +- .../tests/index.test.ts | 82 +++++++++++++------ platforms/react-native/sample/src/App.tsx | 2 +- .../src/hooks/useCheckoutEventHandlers.ts | 6 +- .../sample/src/screens/CartScreen.tsx | 6 +- .../AcceleratedCheckouts_SupportedTests.swift | 10 +++ 16 files changed, 162 insertions(+), 70 deletions(-) diff --git a/platforms/react-native/README.md b/platforms/react-native/README.md index 6107c1c82..d68853636 100644 --- a/platforms/react-native/README.md +++ b/platforms/react-native/README.md @@ -787,7 +787,7 @@ terminal events; nothing needs to be subscribed or torn down explicitly. ```tsx shopify.present(checkoutUrl, { - onClose: () => { + onDismiss: () => { // The sheet was dismissed without a terminal error }, onFail: (error: CheckoutException) => { @@ -798,12 +798,16 @@ shopify.present(checkoutUrl, { | Name | Callback | Fires | | ---------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -| `onClose` | `() => void` | Once, when the buyer dismisses the sheet without a terminal error. | +| `onDismiss` | `() => void` | Once, when the buyer dismisses the sheet without a terminal error. | | `onFail` | `(error: CheckoutException) => void` | Once, when the checkout terminates with an error. | | `onGeolocationRequest` | `(event: GeolocationRequestEvent) => void` | Android only. Fired each time the webview requests geolocation permissions. See [Opting out of the default behavior](#opting-out-of-the-default-behavior). | -`onClose` and `onFail` are mutually exclusive — exactly one of them fires -per `present(...)` call, after which both handles are released. +`onDismiss` and `onFail` are mutually exclusive. At most one fires per +`present(...)` call, after which both handles are released. Calling `dismiss()` +programmatically releases the handles without invoking either callback. +Completion and dismissal are separate events: `CheckoutProtocol.complete` +fires when the order completes, while `onDismiss` fires when the buyer later +dismisses the checkout sheet, including from the confirmation page. ## Identity & customer accounts @@ -1127,20 +1131,25 @@ The `cornerRadius` prop lets you match the buttons to other calls-to-action in y ### Handle loading, errors, and lifecycle events -Attach lifecycle handlers to respond when buyers finish, cancel, or encounter an error. +Attach lifecycle and protocol handlers to respond when buyers complete, +dismiss, or encounter an error. ```tsx +import {CheckoutProtocol} from '@shopify/checkout-kit-react-native'; + { - // Clear cart after successful checkout - clearCart(); + events={{ + [CheckoutProtocol.complete]: () => { + // Clear cart after successful checkout + clearCart(); + }, }} onFail={(error) => { console.error('Accelerated checkout failed:', error); }} - onCancel={() => { - analytics.track('accelerated_checkout_cancelled'); + onDismiss={() => { + analytics.track('accelerated_checkout_dismissed'); }} onRenderStateChange={(event) => { // event.state: 'loading' | 'rendered' | 'error' @@ -1152,6 +1161,10 @@ Attach lifecycle handlers to respond when buyers finish, cancel, or encounter an /> ``` +`onDismiss` reports the checkout presentation lifecycle, including dismissal +from the confirmation page after a successful payment. It does not indicate +whether checkout completed; use `CheckoutProtocol.complete` for that outcome. + --- ## Contributing 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 97d4113a8..e540a7190 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 @@ -286,7 +286,7 @@ export type PreloadState = // @public export interface PresentCallbacks { - onClose?: () => void; + onDismiss?: () => void; onFail?: (error: CheckoutException) => void; onGeolocationRequest?: (event: GeolocationRequestEvent) => void; } diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift index 184773f12..4880ec569 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift @@ -104,7 +104,7 @@ class RCTAcceleratedCheckoutButtonsView: UIView { } @objc var onFail: RCTBubblingEventBlock? - @objc var onCancel: RCTBubblingEventBlock? + @objc var onDismiss: RCTDirectEventBlock? @objc var onRenderStateChange: RCTBubblingEventBlock? @objc var onClickLink: RCTBubblingEventBlock? @objc var onDispatch: RCTDirectEventBlock? @@ -339,7 +339,7 @@ class RCTAcceleratedCheckoutButtonsView: UIView { } private func handleCheckoutDismissed() { - onCancel?([:]) + onDismiss?([:]) } private func handleRenderStateChange(_ state: RenderState) { 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 758abb561..cde81310a 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 @@ -132,9 +132,9 @@ @interface RCT_EXTERN_MODULE (RCTAcceleratedCheckoutButtonsManager, RCTViewManag RCT_EXPORT_VIEW_PROPERTY(onFail, RCTBubblingEventBlock) /** - * Emitted when checkout is cancelled by the buyer. + * Emitted when checkout is dismissed by the buyer. */ -RCT_EXPORT_VIEW_PROPERTY(onCancel, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock) /** * Emitted when the native render state changes. Values: "loading", "rendered", "error". diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/components/AcceleratedCheckoutButtons.tsx b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/components/AcceleratedCheckoutButtons.tsx index f3e97a9c9..956edd869 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/components/AcceleratedCheckoutButtons.tsx +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/components/AcceleratedCheckoutButtons.tsx @@ -85,9 +85,9 @@ interface CommonAcceleratedCheckoutButtonsProps { onFail?: (error: CheckoutException) => void; /** - * Called when checkout is cancelled + * Called when the buyer dismisses checkout, including after completion. */ - onCancel?: () => void; + onDismiss?: () => void; /** * Called when the render state changes @@ -169,7 +169,7 @@ export const AcceleratedCheckoutButtons: React.FC< cornerRadius, wallets, onFail, - onCancel, + onDismiss, onRenderStateChange, onClickLink, events, @@ -188,9 +188,9 @@ export const AcceleratedCheckoutButtons: React.FC< [onFail], ); - const handleCancel = useCallback(() => { - onCancel?.(); - }, [onCancel]); + const handleDismiss = useCallback(() => { + onDismiss?.(); + }, [onDismiss]); const handleRenderStateChange = useCallback( (event: {nativeEvent: unknown}) => { @@ -292,7 +292,7 @@ export const AcceleratedCheckoutButtons: React.FC< cornerRadius={cornerRadius} wallets={wallets} onFail={handleFail} - onCancel={handleCancel} + onDismiss={handleDismiss} onRenderStateChange={handleRenderStateChange} onClickLink={handleClickLink} onDispatch={handleDispatch} 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 735c6e901..1a0faf918 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 @@ -189,8 +189,8 @@ export interface GeolocationRequestEvent { /** * Per-call SDK callbacks for `present(url, callbacks, protocol)`. * - * Exactly one of `onClose` or `onFail` fires per `present(...)` invocation, - * after which the callbacks are released. + * At most one of `onDismiss` or `onFail` fires per `present(...)` invocation, + * after which the callbacks are released. Programmatic dismissal invokes neither. * * `onGeolocationRequest` may fire any number of times during a single * `present(...)` call while the checkout sheet is open. @@ -198,10 +198,12 @@ export interface GeolocationRequestEvent { export interface PresentCallbacks { /** * Fires when the checkout sheet is dismissed without a terminal error. + * This presentation lifecycle event is independent of checkout completion, + * so it also fires when the buyer dismisses the sheet after completing payment. * Mirrors `CheckoutListener.onCheckoutDismissed` on Android * and `CheckoutDelegate.checkoutDidDismiss` on iOS. */ - onClose?: () => void; + onDismiss?: () => void; /** * Fires when the checkout sheet terminates with an error. * Mirrors `CheckoutListener.onCheckoutFailed` on Android @@ -334,9 +336,9 @@ export interface ShopifyCheckoutKit { * Present the checkout. * * @param checkoutURL The URL of the checkout to display. - * @param callbacks Optional per-call SDK callbacks. Exactly one of - * `onClose` or `onFail` fires per call, after which the callbacks are - * released. + * @param callbacks Optional per-call SDK callbacks. At most one of + * `onDismiss` or `onFail` fires per call, after which the callbacks are + * released. Programmatic dismissal invokes neither. * @param protocol Optional per-call Checkout Protocol event handlers. */ present( 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 7907a42a0..a05f0d384 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 @@ -98,7 +98,8 @@ class ShopifyCheckout implements ShopifyCheckoutKit { } /** - * Dismisses the currently displayed checkout sheet + * Dismisses the currently displayed checkout sheet without invoking + * the per-presentation `onDismiss` callback. */ public dismiss(): void { this.releaseDispatchSubscription(); @@ -128,8 +129,9 @@ class ShopifyCheckout implements ShopifyCheckoutKit { /** * Presents the checkout sheet for a given checkout URL. * - * Exactly one of `callbacks.onClose` or `callbacks.onFail` fires per + * At most one of `callbacks.onDismiss` or `callbacks.onFail` fires per * call, after which the per-presentation dispatch subscription is released. + * Programmatic dismissal invokes neither callback. * * @param checkoutUrl The URL of the checkout to display * @param callbacks Optional per-call SDK callbacks diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/present-dispatcher.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/present-dispatcher.ts index d76888560..fe66aaac3 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/present-dispatcher.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/present-dispatcher.ts @@ -190,7 +190,7 @@ function routeSdkLifecycleEvent( ): PresentDispatchResult { switch (type) { case 'close': - callbacks?.onClose?.(); + callbacks?.onDismiss?.(); return {terminal: true}; case 'fail': { const failPayload = validateFailPayload(payload); diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts index 1c8a63373..3c35cfcb9 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts @@ -35,7 +35,7 @@ interface NativeProps extends ViewProps { applePayLabel?: string; applePayStyle?: string; onFail?: BubblingEventHandler; - onCancel?: BubblingEventHandler; + onDismiss?: DirectEventHandler; onRenderStateChange?: BubblingEventHandler; onClickLink?: BubblingEventHandler; onDispatch?: DirectEventHandler; diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/AcceleratedCheckoutButtons.test.tsx b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/AcceleratedCheckoutButtons.test.tsx index a5738a50a..d2349b541 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/AcceleratedCheckoutButtons.test.tsx +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/AcceleratedCheckoutButtons.test.tsx @@ -340,17 +340,46 @@ describe('AcceleratedCheckoutButtons', () => { expect(error.statusCode).toBeUndefined(); }); - it('calls onCancel when native cancel is invoked', () => { - const onCancel = jest.fn(); + it('calls onDismiss when native dismissal is invoked', () => { + const onDismiss = jest.fn(); const {getByTestId} = render( , ); const nativeComponent = getByTestId('accelerated-checkout-buttons'); - nativeComponent.props.onCancel(); - expect(onCancel).toHaveBeenCalled(); + nativeComponent.props.onDismiss(); + expect(onDismiss).toHaveBeenCalledTimes(1); + }); + + it('delivers completion before a later dismissal as separate events', () => { + const onComplete = jest.fn(); + const onDismiss = jest.fn(); + const {getByTestId} = render( + , + ); + const nativeComponent = getByTestId('accelerated-checkout-buttons'); + + nativeComponent.props.onDispatch({ + nativeEvent: { + value: JSON.stringify({ + type: CheckoutProtocol.complete, + payload: {...wireCheckout, status: 'completed'}, + }), + }, + }); + + expect(onComplete).toHaveBeenCalledTimes(1); + expect(onDismiss).not.toHaveBeenCalled(); + + nativeComponent.props.onDismiss(); + + expect(onDismiss).toHaveBeenCalledTimes(1); }); it('maps render state change to typed states including error reason', () => { @@ -448,7 +477,7 @@ describe('AcceleratedCheckoutButtons', () => { it('handles callbacks without throwing', () => { const mockCallbacks = { onFail: jest.fn(), - onCancel: jest.fn(), + onDismiss: jest.fn(), onRenderStateChange: jest.fn(), onClickLink: jest.fn(), }; 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 995ecb490..8e1898919 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 @@ -195,12 +195,12 @@ describe('useShopifyCheckout', () => { , ); - const onClose = jest.fn(); + const onDismiss = jest.fn(); const onFail = jest.fn(); const onGeolocationRequest = jest.fn(); act(() => { - hookValue.present(checkoutUrl, {onClose, onFail, onGeolocationRequest}); + hookValue.present(checkoutUrl, {onDismiss, onFail, onGeolocationRequest}); }); expect(NativeModules.ShopifyCheckoutKit.onDispatch).toHaveBeenCalledWith( 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 f8f63e152..41b8b019d 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 @@ -401,7 +401,7 @@ describe('ShopifyCheckoutKit', () => { it('calls `present` with a dispatcher when callbacks are provided', () => { const instance = new ShopifyCheckout(); - instance.present(checkoutUrl, {onClose: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); expect(NativeModule.present).toHaveBeenCalledWith(checkoutUrl, []); expect(NativeModule.onDispatch).toHaveBeenCalledWith( expect.any(Function), @@ -416,8 +416,8 @@ describe('ShopifyCheckoutKit', () => { .mockReturnValueOnce(secondSubscription); const instance = new ShopifyCheckout(); - instance.present(checkoutUrl, {onClose: jest.fn()}); - instance.present(checkoutUrl, {onClose: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); expect(firstSubscription.remove).toHaveBeenCalledTimes(1); expect(secondSubscription.remove).not.toHaveBeenCalled(); @@ -428,21 +428,21 @@ describe('ShopifyCheckoutKit', () => { NativeModule.onDispatch.mockReturnValueOnce(subscription); const instance = new ShopifyCheckout(); - instance.present(checkoutUrl, {onClose: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); lastDispatch()(JSON.stringify({type: 'close'})); expect(subscription.remove).toHaveBeenCalledTimes(1); }); - it('invokes `onClose` when the dispatcher receives a close envelope', () => { + it('invokes `onDismiss` when the dispatcher receives a close envelope', () => { const instance = new ShopifyCheckout(); - const onClose = jest.fn(); - instance.present(checkoutUrl, {onClose}); + const onDismiss = jest.fn(); + instance.present(checkoutUrl, {onDismiss}); lastDispatch()(JSON.stringify({type: 'close'})); - expect(onClose).toHaveBeenCalledTimes(1); + expect(onDismiss).toHaveBeenCalledTimes(1); }); - it('ignores a close envelope when no `onClose` handler was provided', () => { + it('ignores a close envelope when no `onDismiss` handler was provided', () => { const instance = new ShopifyCheckout(); instance.present(checkoutUrl, {onFail: jest.fn()}); expect(() => @@ -521,8 +521,8 @@ describe('ShopifyCheckoutKit', () => { it('ignores a fail envelope when no `onFail` handler was provided', () => { const instance = new ShopifyCheckout(); - const onClose = jest.fn(); - instance.present(checkoutUrl, {onClose}); + const onDismiss = jest.fn(); + instance.present(checkoutUrl, {onDismiss}); expect(() => lastDispatch()( JSON.stringify({type: 'fail', payload: sdkError}), @@ -608,6 +608,32 @@ describe('ShopifyCheckoutKit', () => { expect(onStart.mock.calls[0][0].id).toBe('chk_123'); }); + it('delivers completion before a later sheet dismissal as separate events', () => { + const instance = new ShopifyCheckout(); + const onComplete = jest.fn(); + const onDismiss = jest.fn(); + instance.present( + checkoutUrl, + {onDismiss}, + {[CheckoutProtocol.complete]: onComplete}, + ); + const dispatch = lastDispatch(); + + dispatch( + JSON.stringify({ + type: CheckoutProtocol.complete, + payload: {...wireStartPayload, status: 'completed'}, + }), + ); + + expect(onComplete).toHaveBeenCalledTimes(1); + expect(onDismiss).not.toHaveBeenCalled(); + + dispatch(JSON.stringify({type: 'close'})); + + expect(onDismiss).toHaveBeenCalledTimes(1); + }); + it('passes subscribedMethods to native present()', () => { const instance = new ShopifyCheckout(); instance.present(checkoutUrl, undefined, { @@ -624,13 +650,13 @@ describe('ShopifyCheckoutKit', () => { it('still routes existing close/fail/geolocationRequest cases alongside protocol handlers', () => { Platform.OS = 'ios'; const instance = new ShopifyCheckout(); - const onClose = jest.fn(); + const onDismiss = jest.fn(); const onFail = jest.fn(); const onGeolocationRequest = jest.fn(); const onStart = jest.fn(); instance.present( checkoutUrl, - {onClose, onFail, onGeolocationRequest}, + {onDismiss, onFail, onGeolocationRequest}, {[CheckoutProtocol.start]: onStart}, ); const dispatch = lastDispatch(); @@ -651,7 +677,7 @@ describe('ShopifyCheckoutKit', () => { payload: {origin: 'https://shopify.com'}, }), ); - expect(onClose).toHaveBeenCalledTimes(1); + expect(onDismiss).toHaveBeenCalledTimes(1); expect(onFail).toHaveBeenCalledTimes(1); expect(onFail.mock.calls[0][0]).toBeInstanceOf(CheckoutException); expect(onGeolocationRequest).toHaveBeenCalledWith({ @@ -665,10 +691,10 @@ describe('ShopifyCheckoutKit', () => { describe('envelope parsing', () => { it('logs a LifecycleEventParseError when the envelope is invalid JSON', () => { const instance = new ShopifyCheckout(); - const onClose = jest.fn(); - instance.present(checkoutUrl, {onClose}); + const onDismiss = jest.fn(); + instance.present(checkoutUrl, {onDismiss}); lastDispatch()('not-json'); - expect(onClose).not.toHaveBeenCalled(); + expect(onDismiss).not.toHaveBeenCalled(); expect(console.error).toHaveBeenCalledWith( expect.any(LifecycleEventParseError), 'not-json', @@ -677,13 +703,13 @@ describe('ShopifyCheckoutKit', () => { it('warns via console.warn for envelopes with unknown `type` values', () => { const instance = new ShopifyCheckout(); - const onClose = jest.fn(); + const onDismiss = jest.fn(); const onFail = jest.fn(); - instance.present(checkoutUrl, {onClose, onFail}); + instance.present(checkoutUrl, {onDismiss, onFail}); expect(() => lastDispatch()(JSON.stringify({type: 'unknown', payload: {}})), ).not.toThrow(); - expect(onClose).not.toHaveBeenCalled(); + expect(onDismiss).not.toHaveBeenCalled(); expect(onFail).not.toHaveBeenCalled(); expect(console.warn).toHaveBeenCalledWith( expect.stringContaining('unknown type "unknown"'), @@ -692,7 +718,7 @@ describe('ShopifyCheckoutKit', () => { it('logs a LifecycleEventParseError when the envelope is missing a string `type`', () => { const instance = new ShopifyCheckout(); - instance.present(checkoutUrl, {onClose: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); lastDispatch()(JSON.stringify({payload: {}})); expect(console.error).toHaveBeenCalledWith( expect.any(LifecycleEventParseError), @@ -790,6 +816,16 @@ describe('ShopifyCheckoutKit', () => { instance.dismiss(); expect(NativeModule.dismiss).toHaveBeenCalledTimes(1); }); + + it('does not invoke `onDismiss` for programmatic dismissal', () => { + const instance = new ShopifyCheckout(); + const onDismiss = jest.fn(); + instance.present(checkoutUrl, {onDismiss}); + + instance.dismiss(); + + expect(onDismiss).not.toHaveBeenCalled(); + }); }); describe('getConfig', () => { @@ -972,7 +1008,7 @@ describe('ShopifyCheckoutKit', () => { const instance = new ShopifyCheckout(undefined, { handleGeolocationRequests: false, }); - instance.present(checkoutUrl, {onClose: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); lastDispatch()(geolocationEnvelope); await flush(); @@ -1000,7 +1036,7 @@ describe('ShopifyCheckoutKit', () => { it('does not run the default geolocation handler on iOS even if dispatcher fires', async () => { const instance = new ShopifyCheckout(); - instance.present(checkoutUrl, {onClose: jest.fn()}); + instance.present(checkoutUrl, {onDismiss: jest.fn()}); lastDispatch()(geolocationEnvelope); await flush(); diff --git a/platforms/react-native/sample/src/App.tsx b/platforms/react-native/sample/src/App.tsx index c28eca034..61ab32796 100644 --- a/platforms/react-native/sample/src/App.tsx +++ b/platforms/react-native/sample/src/App.tsx @@ -453,7 +453,7 @@ function Routes() { // Checkout URLs case storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage(): shopify.present(url, { - onClose: () => eventHandlers.onCancel?.(), + onDismiss: () => eventHandlers.onDismiss?.(), onFail: error => eventHandlers.onFail?.(error), }); return; diff --git a/platforms/react-native/sample/src/hooks/useCheckoutEventHandlers.ts b/platforms/react-native/sample/src/hooks/useCheckoutEventHandlers.ts index 5a6c535f8..63cd8ea5a 100644 --- a/platforms/react-native/sample/src/hooks/useCheckoutEventHandlers.ts +++ b/platforms/react-native/sample/src/hooks/useCheckoutEventHandlers.ts @@ -10,7 +10,7 @@ import {Linking} from 'react-native'; interface EventHandlers { onFail?: (error: CheckoutException) => void; - onCancel?: () => void; + onDismiss?: () => void; onRenderStateChange?: (event: RenderStateChangeEvent) => void; onClickLink?: (url: string) => void; } @@ -45,8 +45,8 @@ export function useShopifyEventHandlers(name?: string): EventHandlers { onFail: error => { log('onFail', error); }, - onCancel: () => { - log('onCancel'); + onDismiss: () => { + log('onDismiss'); }, onRenderStateChange: event => { log('onRenderStateChange', event); diff --git a/platforms/react-native/sample/src/screens/CartScreen.tsx b/platforms/react-native/sample/src/screens/CartScreen.tsx index 05c75fdbf..274e6dd42 100644 --- a/platforms/react-native/sample/src/screens/CartScreen.tsx +++ b/platforms/react-native/sample/src/screens/CartScreen.tsx @@ -52,7 +52,7 @@ function CartScreen(): React.JSX.Element { const {queries} = useShopify(); const {appConfig} = useConfig(); // Separate handler instances so debug logs are labelled with the actual - // surface that emitted the event. Otherwise an `onClose` from the + // surface that emitted the event. Otherwise an `onDismiss` from the // `ShopifyCheckout.present()` sheet would log under the // `AcceleratedCheckoutButtons` namespace and confuse anyone debugging. const sheetEventHandlers = useShopifyEventHandlers('Cart - CheckoutSheet'); @@ -128,8 +128,8 @@ function CartScreen(): React.JSX.Element { present( checkoutURL, { - onClose: () => { - sheetEventHandlers.onCancel?.(); + onDismiss: () => { + sheetEventHandlers.onDismiss?.(); }, onFail: error => { sheetEventHandlers.onFail?.(error); diff --git a/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/AcceleratedCheckouts_SupportedTests.swift b/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/AcceleratedCheckouts_SupportedTests.swift index 65d3129c8..f5c3c82d7 100644 --- a/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/AcceleratedCheckouts_SupportedTests.swift +++ b/platforms/react-native/test/rct-integration-app/RCTIntegrationAppTests/AcceleratedCheckouts_SupportedTests.swift @@ -1,5 +1,6 @@ import Foundation import PassKit +import React @testable import RNShopifyCheckoutKit @testable import ShopifyCheckoutKit import SwiftUI @@ -60,6 +61,15 @@ class AcceleratedCheckouts_SupportedTests: XCTestCase { ).boolValue } + func testDismissIsRegisteredAsDirectEvent() throws { + let viewConfig = RCTComponentData.viewConfig(forViewMangerClass: RCTAcceleratedCheckoutButtonsManager.self) + let directEvents = try XCTUnwrap(viewConfig["directEvents"] as? [String]) + let bubblingEvents = try XCTUnwrap(viewConfig["bubblingEvents"] as? [String]) + + XCTAssertTrue(directEvents.contains("topDismiss")) + XCTAssertFalse(bubblingEvents.contains("topDismiss")) + } + func testConfigureAcceleratedCheckoutsSetsSharedConfigsOnIOS16() { let notificationExpectation = expectation(forNotification: Notification.Name("AcceleratedCheckoutConfigurationUpdated"), object: nil, handler: nil) configureAcceleratedCheckouts(includeApplePay: true)