diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 34cb204a..25e7b5af 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -39,10 +39,27 @@ const requireNativeComponent = (..._args: any[]) => { }); }; +const Text = (props: any) => { + const React = require('react'); + return React.createElement('Text', props, props.children); +}; + const StyleSheet = { flatten: jest.fn(style => style), }; +const UIManager = { + getViewManagerConfig: jest.fn(() => ({ + Commands: { + respondToEvent: 'respondToEvent', + reload: 'reload', + }, + })), + dispatchViewManagerCommand: jest.fn(), +}; + +const findNodeHandle = jest.fn(() => 1); + const exampleConfig = {preloading: true}; const ShopifyCheckoutSheetKit = { @@ -52,12 +69,12 @@ const ShopifyCheckoutSheetKit = { dismiss: jest.fn(), invalidateCache: jest.fn(), getConfig: jest.fn(async () => exampleConfig), - setConfig: jest.fn(), + setConfig: jest.fn(async () => {}), addEventListener: jest.fn(), removeEventListeners: jest.fn(), initiateGeolocationRequest: jest.fn(), configureAcceleratedCheckouts: jest.fn(), - isAcceleratedCheckoutAvailable: jest.fn(), + isAcceleratedCheckoutAvailable: jest.fn(async () => false), }; // CommonJS export for Jest manual mock resolution @@ -75,4 +92,7 @@ module.exports = { }, }, StyleSheet, + UIManager, + findNodeHandle, + Text, }; diff --git a/modules/@shopify/checkout-sheet-kit/package.snapshot.json b/modules/@shopify/checkout-sheet-kit/package.snapshot.json index 674d29b5..a3243aa8 100644 --- a/modules/@shopify/checkout-sheet-kit/package.snapshot.json +++ b/modules/@shopify/checkout-sheet-kit/package.snapshot.json @@ -40,8 +40,8 @@ "lib/commonjs/index.d.js.map", "lib/commonjs/index.js", "lib/commonjs/index.js.map", - "lib/commonjs/ShopifyCheckoutEventProvider.js", - "lib/commonjs/ShopifyCheckoutEventProvider.js.map", + "lib/commonjs/native/RCTCheckoutWebView.js", + "lib/commonjs/native/RCTCheckoutWebView.js.map", "lib/module/components/AcceleratedCheckoutButtons.js", "lib/module/components/AcceleratedCheckoutButtons.js.map", "lib/module/components/ShopifyCheckout.js", @@ -56,8 +56,8 @@ "lib/module/index.d.js.map", "lib/module/index.js", "lib/module/index.js.map", - "lib/module/ShopifyCheckoutEventProvider.js", - "lib/module/ShopifyCheckoutEventProvider.js.map", + "lib/module/native/RCTCheckoutWebView.js", + "lib/module/native/RCTCheckoutWebView.js.map", "lib/typescript/src/components/AcceleratedCheckoutButtons.d.ts", "lib/typescript/src/components/AcceleratedCheckoutButtons.d.ts.map", "lib/typescript/src/components/ShopifyCheckout.d.ts", @@ -66,8 +66,10 @@ "lib/typescript/src/context.d.ts.map", "lib/typescript/src/index.d.ts", "lib/typescript/src/index.d.ts.map", - "lib/typescript/src/ShopifyCheckoutEventProvider.d.ts", - "lib/typescript/src/ShopifyCheckoutEventProvider.d.ts.map", + "lib/typescript/src/native/__mocks__/RCTCheckoutWebView.d.ts", + "lib/typescript/src/native/__mocks__/RCTCheckoutWebView.d.ts.map", + "lib/typescript/src/native/RCTCheckoutWebView.d.ts", + "lib/typescript/src/native/RCTCheckoutWebView.d.ts.map", "package.json", "src/components/AcceleratedCheckoutButtons.tsx", "src/components/ShopifyCheckout.tsx", @@ -76,5 +78,6 @@ "src/events.d.ts", "src/index.d.ts", "src/index.ts", - "src/ShopifyCheckoutEventProvider.tsx" + "src/native/__mocks__/RCTCheckoutWebView.ts", + "src/native/RCTCheckoutWebView.ts" ] diff --git a/modules/@shopify/checkout-sheet-kit/src/ShopifyCheckoutEventProvider.tsx b/modules/@shopify/checkout-sheet-kit/src/ShopifyCheckoutEventProvider.tsx deleted file mode 100644 index b46e5953..00000000 --- a/modules/@shopify/checkout-sheet-kit/src/ShopifyCheckoutEventProvider.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* -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. -*/ - -import React, {createContext, useContext, useRef, useCallback} from 'react'; -import {UIManager, findNodeHandle} from 'react-native'; - -interface CheckoutEventContextType { - registerWebView: (webViewRef: React.RefObject) => void; - unregisterWebView: () => void; - respondToEvent: (eventId: string, response: any) => Promise; -} - -const CheckoutEventContext = createContext( - null, -); - -export interface ShopifyCheckoutEventProviderProps { - children: React.ReactNode; -} - -/** - * ShopifyCheckoutEventProvider manages active checkout events and provides methods to respond to them. - * This provider maintains references to active events and the webview to enable native callbacks. - */ -export const ShopifyCheckoutEventProvider = ({ - children, -}: ShopifyCheckoutEventProviderProps) => { - const webViewRef = useRef | null>(null); - - const registerWebView = useCallback((ref: React.RefObject) => { - webViewRef.current = ref; - }, []); - - const unregisterWebView = useCallback(() => { - webViewRef.current = null; - }, []); - - const respondToEvent = useCallback( - async (eventId: string, response: any): Promise => { - if (!webViewRef.current?.current) { - return false; - } - - try { - const handle = findNodeHandle(webViewRef.current.current); - if (!handle) { - return false; - } - - const viewConfig = UIManager.getViewManagerConfig('RCTCheckoutWebView'); - const commandId = - viewConfig?.Commands?.respondToEvent ?? 'respondToEvent'; - - // Call the native method to respond to the event - UIManager.dispatchViewManagerCommand(handle, commandId, [ - eventId, - JSON.stringify(response), - ]); - - return true; - } catch (error) { - return false; - } - }, - [], - ); - - const contextValue: CheckoutEventContextType = { - registerWebView, - unregisterWebView, - respondToEvent, - }; - - return ( - - {children} - - ); -}; - -/** - * Hook to access checkout event functionality - */ -export function useCheckoutEvents(): CheckoutEventContextType | null { - const context = useContext(CheckoutEventContext); - return context; -} - -/** - * Enhanced hook for working with specific Shopify checkout events - * @param eventId The ID of the event to work with - */ -export function useShopifyEvent(eventId: string) { - const eventContext = useCheckoutEvents(); - - return { - id: eventId, - respondWith: useCallback( - async (response: any) => { - if (!eventContext) { - return false; - } - return await eventContext.respondToEvent(eventId, response); - }, - [eventId, eventContext], - ), - }; -} diff --git a/modules/@shopify/checkout-sheet-kit/src/components/ShopifyCheckout.tsx b/modules/@shopify/checkout-sheet-kit/src/components/ShopifyCheckout.tsx index e82c6998..c112a978 100644 --- a/modules/@shopify/checkout-sheet-kit/src/components/ShopifyCheckout.tsx +++ b/modules/@shopify/checkout-sheet-kit/src/components/ShopifyCheckout.tsx @@ -22,15 +22,13 @@ import React, { useCallback, useImperativeHandle, forwardRef, - useEffect, } from 'react'; +import {UIManager, findNodeHandle, type ViewStyle} from 'react-native'; +import {useWebviewRegistration} from '../context'; import { - requireNativeComponent, - UIManager, - findNodeHandle, - type ViewStyle, -} from 'react-native'; -import {useCheckoutEvents} from '../ShopifyCheckoutEventProvider'; + RCTCheckoutWebView, + type NativeShopifyCheckoutWebViewProps, +} from '../native/RCTCheckoutWebView'; import type { CheckoutAddressChangeStartEvent, CheckoutCompleteEvent, @@ -38,11 +36,7 @@ import type { CheckoutStartEvent, CheckoutSubmitStartEvent, } from '../events.d'; -import { - parseCheckoutError, - type CheckoutException, - type CheckoutNativeError, -} from '../errors.d'; +import {parseCheckoutError, type CheckoutException} from '../errors.d'; export interface ShopifyCheckoutProps { /** @@ -123,30 +117,6 @@ export interface ShopifyCheckoutRef { reload: () => void; } -interface NativeShopifyCheckoutWebViewProps { - checkoutUrl: string; - auth?: string; - style?: ViewStyle; - testID?: string; - onStart?: (event: {nativeEvent: CheckoutStartEvent}) => void; - onFail?: (event: {nativeEvent: CheckoutNativeError}) => void; - onComplete?: (event: {nativeEvent: CheckoutCompleteEvent}) => void; - onCancel?: () => void; - onLinkClick?: (event: {nativeEvent: {url: string}}) => void; - onAddressChangeStart?: (event: { - nativeEvent: CheckoutAddressChangeStartEvent; - }) => void; - onSubmitStart?: (event: {nativeEvent: CheckoutSubmitStartEvent}) => void; - onPaymentMethodChangeStart?: (event: { - nativeEvent: CheckoutPaymentMethodChangeStartEvent; - }) => void; -} - -const RCTCheckoutWebView = - requireNativeComponent( - 'RCTCheckoutWebView', - ); - /** * Checkout provides a native webview component for displaying * Shopify checkout pages directly within your React Native app. @@ -211,16 +181,7 @@ export const ShopifyCheckout = forwardRef< ) => { const webViewRef = useRef>(null); - const eventContext = useCheckoutEvents(); - - // Register webview reference with the event provider - useEffect(() => { - if (!eventContext) return; - - eventContext.registerWebView(webViewRef); - - return () => eventContext.unregisterWebView(); - }, [eventContext]); + useWebviewRegistration(webViewRef); const handleStart = useCallback< Required['onStart'] diff --git a/modules/@shopify/checkout-sheet-kit/src/context.tsx b/modules/@shopify/checkout-sheet-kit/src/context.tsx index 956d8212..b1a4257d 100644 --- a/modules/@shopify/checkout-sheet-kit/src/context.tsx +++ b/modules/@shopify/checkout-sheet-kit/src/context.tsx @@ -23,7 +23,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO import React, {useCallback, useMemo, useRef, useEffect, useState} from 'react'; import type {PropsWithChildren} from 'react'; -import {type EmitterSubscription} from 'react-native'; +import { + type EmitterSubscription, + UIManager, + findNodeHandle, +} from 'react-native'; import {ShopifyCheckoutSheet} from './index'; import type {Features} from './index.d'; import type { @@ -47,10 +51,61 @@ interface Context { dismiss: () => void; invalidate: () => void; version: Maybe; + respondToEvent: (eventId: string, response: any) => Promise; } -const ShopifyCheckoutSheetContext = React.createContext( - null as unknown as Context, +interface InternalContext extends Context { + registerWebView: (ref: React.RefObject) => void; + unregisterWebView: () => void; +} + +interface WebviewState { + registerWebView: (ref: React.RefObject) => void; + unregisterWebView: () => void; + respondToEvent: (eventId: string, response: any) => Promise; +} + +function useWebview(): WebviewState { + const webViewRef = useRef | null>(null); + + const registerWebView = useCallback((ref: React.RefObject) => { + webViewRef.current = ref; + }, []); + + const unregisterWebView = useCallback(() => { + webViewRef.current = null; + }, []); + + const respondToEvent = useCallback( + async (eventId: string, response: any): Promise => { + if (!webViewRef.current?.current) { + return false; + } + try { + const handle = findNodeHandle(webViewRef.current.current); + if (!handle) { + return false; + } + const viewConfig = UIManager.getViewManagerConfig('RCTCheckoutWebView'); + const commandId = + viewConfig?.Commands?.respondToEvent ?? 'respondToEvent'; + UIManager.dispatchViewManagerCommand(handle, commandId, [ + eventId, + JSON.stringify(response), + ]); + return true; + } catch { + return false; + } + }, + [], + ); + + return {registerWebView, unregisterWebView, respondToEvent}; +} + +const ShopifyCheckoutSheetContext = React.createContext( + null as unknown as InternalContext, ); interface Props { @@ -66,6 +121,7 @@ export function ShopifyCheckoutSheetProvider({ const [acceleratedCheckoutsAvailable, setAcceleratedCheckoutsAvailable] = useState(false); const instance = useRef(null); + const webview = useWebview(); if (!instance.current) { instance.current = new ShopifyCheckoutSheet(configuration, features); @@ -131,7 +187,7 @@ export function ShopifyCheckoutSheetProvider({ return instance.current?.getConfig(); }, []); - const context = useMemo((): Context => { + const context = useMemo((): InternalContext => { return { acceleratedCheckoutsAvailable, addEventListener, @@ -143,6 +199,9 @@ export function ShopifyCheckoutSheetProvider({ invalidate, removeEventListeners, version: instance.current?.version, + registerWebView: webview.registerWebView, + unregisterWebView: webview.unregisterWebView, + respondToEvent: webview.respondToEvent, }; }, [ acceleratedCheckoutsAvailable, @@ -154,6 +213,9 @@ export function ShopifyCheckoutSheetProvider({ preload, present, invalidate, + webview.registerWebView, + webview.unregisterWebView, + webview.respondToEvent, ]); return ( @@ -163,14 +225,53 @@ export function ShopifyCheckoutSheetProvider({ ); } -export function useShopifyCheckoutSheet() { +export function useShopifyCheckoutSheet(): Context { const context = React.useContext(ShopifyCheckoutSheetContext); if (!context) { throw new Error( 'useShopifyCheckoutSheet must be used from within a ShopifyCheckoutSheetContext', ); } - return context; + return useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- destructuring to remove from return type + const {registerWebView, unregisterWebView, ...publicContext} = context; + return publicContext; + }, [context]); +} + +export function useWebviewRegistration(webViewRef: React.RefObject) { + const context = React.useContext(ShopifyCheckoutSheetContext); + if (!context) { + throw new Error( + 'useWebviewRegistration must be used within ShopifyCheckoutSheetProvider', + ); + } + + useEffect(() => { + context.registerWebView(webViewRef); + return () => context.unregisterWebView(); + }, [context, webViewRef]); +} + +export function useShopifyEvent(eventId: string) { + const context = React.useContext(ShopifyCheckoutSheetContext); + if (!context) { + throw new Error( + 'useShopifyEvent must be used within ShopifyCheckoutSheetProvider', + ); + } + + const respondWith = useCallback( + async (response: any) => { + return await context.respondToEvent(eventId, response); + }, + [eventId, context], + ); + + return { + id: eventId, + respondWith, + }; } export default ShopifyCheckoutSheetContext; diff --git a/modules/@shopify/checkout-sheet-kit/src/index.ts b/modules/@shopify/checkout-sheet-kit/src/index.ts index 1ef56e5d..5a0fe660 100644 --- a/modules/@shopify/checkout-sheet-kit/src/index.ts +++ b/modules/@shopify/checkout-sheet-kit/src/index.ts @@ -32,7 +32,11 @@ import type { EventSubscription, PermissionStatus, } from 'react-native'; -import {ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet} from './context'; +import { + ShopifyCheckoutSheetProvider, + useShopifyCheckoutSheet, + useShopifyEvent, +} from './context'; import {ApplePayContactField, ColorScheme} from './index.d'; import type { AcceleratedCheckoutConfiguration, @@ -205,11 +209,7 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit { parseCheckoutError, ); break; - case 'addressChangeStart': - case 'complete': case 'geolocationRequest': - case 'start': - case 'submitStart': eventCallback = this.parseEventData(event, callback); break; case 'close': @@ -461,6 +461,7 @@ export { ShopifyCheckoutSheet, ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet, + useShopifyEvent, }; // Error classes @@ -519,16 +520,9 @@ export type { ShopifyCheckoutProps, ShopifyCheckoutRef, } from './components/ShopifyCheckout'; -export type {ShopifyCheckoutEventProviderProps} from './ShopifyCheckoutEventProvider'; - // Components export { AcceleratedCheckoutButtons, RenderState, } from './components/AcceleratedCheckoutButtons'; export {ShopifyCheckout} from './components/ShopifyCheckout'; -export { - ShopifyCheckoutEventProvider, - useCheckoutEvents, - useShopifyEvent, -} from './ShopifyCheckoutEventProvider'; diff --git a/modules/@shopify/checkout-sheet-kit/src/native/RCTCheckoutWebView.ts b/modules/@shopify/checkout-sheet-kit/src/native/RCTCheckoutWebView.ts new file mode 100644 index 00000000..b7281991 --- /dev/null +++ b/modules/@shopify/checkout-sheet-kit/src/native/RCTCheckoutWebView.ts @@ -0,0 +1,57 @@ +/* +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. +*/ + +import {requireNativeComponent} from 'react-native'; +import type {ViewStyle} from 'react-native'; +import type { + CheckoutStartEvent, + CheckoutCompleteEvent, + CheckoutAddressChangeStartEvent, + CheckoutSubmitStartEvent, + CheckoutPaymentMethodChangeStartEvent, +} from '../events.d'; +import type {CheckoutNativeError} from '../errors.d'; + +export interface NativeShopifyCheckoutWebViewProps { + checkoutUrl: string; + auth?: string; + style?: ViewStyle; + testID?: string; + onStart?: (event: {nativeEvent: CheckoutStartEvent}) => void; + onFail?: (event: {nativeEvent: CheckoutNativeError}) => void; + onComplete?: (event: {nativeEvent: CheckoutCompleteEvent}) => void; + onCancel?: () => void; + onLinkClick?: (event: {nativeEvent: {url: string}}) => void; + onAddressChangeStart?: (event: { + nativeEvent: CheckoutAddressChangeStartEvent; + }) => void; + onSubmitStart?: (event: {nativeEvent: CheckoutSubmitStartEvent}) => void; + onPaymentMethodChangeStart?: (event: { + nativeEvent: CheckoutPaymentMethodChangeStartEvent; + }) => void; +} + +export const RCTCheckoutWebView = + requireNativeComponent( + 'RCTCheckoutWebView', + ); diff --git a/modules/@shopify/checkout-sheet-kit/src/native/__mocks__/RCTCheckoutWebView.ts b/modules/@shopify/checkout-sheet-kit/src/native/__mocks__/RCTCheckoutWebView.ts new file mode 100644 index 00000000..a8317707 --- /dev/null +++ b/modules/@shopify/checkout-sheet-kit/src/native/__mocks__/RCTCheckoutWebView.ts @@ -0,0 +1,26 @@ +/* +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. +*/ + +import React from 'react'; +export const RCTCheckoutWebView = (props: any) => + React.createElement('View', props); diff --git a/modules/@shopify/checkout-sheet-kit/tests/CheckoutAddressChange.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/CheckoutAddressChange.test.tsx index af447c35..ba7d8f07 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/CheckoutAddressChange.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/CheckoutAddressChange.test.tsx @@ -1,30 +1,16 @@ -// Mock the native view component BEFORE imports -jest.mock('react-native', () => { - const RN = jest.requireActual('react-native'); - const React = jest.requireActual('react'); - - RN.UIManager.getViewManagerConfig = jest.fn(() => ({ - Commands: {}, - })); - - // Create mock component - const MockRCTCheckoutWebView = (props: any) => { - return React.createElement('View', props); - }; - - return Object.setPrototypeOf( - { - requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView), - }, - RN, - ); -}); +jest.mock('../src/native/RCTCheckoutWebView'); +jest.mock('react-native'); import React from 'react'; import {render, act} from '@testing-library/react-native'; import {ShopifyCheckout} from '../src/components/ShopifyCheckout'; +import {ShopifyCheckoutSheetProvider} from '../src/context'; import {createTestCart} from './testFixtures'; +const Wrapper = ({children}: {children: React.ReactNode}) => ( + {children} +); + describe('Checkout Component - Address Change Events', () => { const mockCheckoutUrl = 'https://example.myshopify.com/checkout'; @@ -37,6 +23,7 @@ describe('Checkout Component - Address Change Events', () => { onAddressChangeStart={onAddressChangeStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -69,6 +56,7 @@ describe('Checkout Component - Address Change Events', () => { checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -96,6 +84,7 @@ describe('Checkout Component - Address Change Events', () => { onAddressChangeStart={onAddressChangeStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -116,6 +105,7 @@ describe('Checkout Component - Address Change Events', () => { onAddressChangeStart={onAddressChangeStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); diff --git a/modules/@shopify/checkout-sheet-kit/tests/CheckoutComplete.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/CheckoutComplete.test.tsx index 8d5f5031..b150be8c 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/CheckoutComplete.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/CheckoutComplete.test.tsx @@ -1,30 +1,16 @@ -// Mock the native view component BEFORE imports -jest.mock('react-native', () => { - const RN = jest.requireActual('react-native'); - const React = jest.requireActual('react'); - - RN.UIManager.getViewManagerConfig = jest.fn(() => ({ - Commands: {}, - })); - - // Create mock component - const MockRCTCheckoutWebView = (props: any) => { - return React.createElement('View', props); - }; - - return Object.setPrototypeOf( - { - requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView), - }, - RN, - ); -}); +jest.mock('../src/native/RCTCheckoutWebView'); +jest.mock('react-native'); import React from 'react'; import {render, act} from '@testing-library/react-native'; import {ShopifyCheckout} from '../src/components/ShopifyCheckout'; +import {ShopifyCheckoutSheetProvider} from '../src/context'; import {createTestCart} from './testFixtures'; +const Wrapper = ({children}: {children: React.ReactNode}) => ( + {children} +); + describe('Checkout Component - Complete Events', () => { const mockCheckoutUrl = 'https://example.myshopify.com/checkout'; @@ -46,6 +32,7 @@ describe('Checkout Component - Complete Events', () => { onComplete={onComplete} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -80,6 +67,7 @@ describe('Checkout Component - Complete Events', () => { onComplete={onComplete} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -107,6 +95,7 @@ describe('Checkout Component - Complete Events', () => { onComplete={onComplete} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -136,6 +125,7 @@ describe('Checkout Component - Complete Events', () => { onComplete={onComplete} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -165,6 +155,7 @@ describe('Checkout Component - Complete Events', () => { checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); diff --git a/modules/@shopify/checkout-sheet-kit/tests/CheckoutError.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/CheckoutError.test.tsx index d8ad5eb5..d30dcbc8 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/CheckoutError.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/CheckoutError.test.tsx @@ -1,28 +1,10 @@ -// Mock the native view component BEFORE imports -jest.mock('react-native', () => { - const RN = jest.requireActual('react-native'); - const React = jest.requireActual('react'); - - RN.UIManager.getViewManagerConfig = jest.fn(() => ({ - Commands: {}, - })); - - // Create mock component - const MockRCTCheckoutWebView = (props: any) => { - return React.createElement('View', props); - }; - - return Object.setPrototypeOf( - { - requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView), - }, - RN, - ); -}); +jest.mock('../src/native/RCTCheckoutWebView'); +jest.mock('react-native'); import React from 'react'; import {render, act} from '@testing-library/react-native'; import {ShopifyCheckout} from '../src/components/ShopifyCheckout'; +import {ShopifyCheckoutSheetProvider} from '../src/context'; import { CheckoutErrorCode, ConfigurationError, @@ -33,6 +15,10 @@ import { GenericError, } from '../src/errors.d'; +const Wrapper = ({children}: {children: React.ReactNode}) => ( + {children} +); + describe('Checkout Component - Error Events', () => { const mockCheckoutUrl = 'https://example.myshopify.com/checkout'; @@ -46,6 +32,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -83,6 +70,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -116,6 +104,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -199,6 +188,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -226,6 +216,7 @@ describe('Checkout Component - Error Events', () => { checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -254,6 +245,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -291,6 +283,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -328,6 +321,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -365,6 +359,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -403,6 +398,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -439,6 +435,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -475,6 +472,7 @@ describe('Checkout Component - Error Events', () => { onFail={onFail} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); diff --git a/modules/@shopify/checkout-sheet-kit/tests/CheckoutStart.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/CheckoutStart.test.tsx index 1e38262a..d75b01fb 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/CheckoutStart.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/CheckoutStart.test.tsx @@ -1,30 +1,16 @@ -// Mock the native view component BEFORE imports -jest.mock('react-native', () => { - const RN = jest.requireActual('react-native'); - const React = jest.requireActual('react'); - - RN.UIManager.getViewManagerConfig = jest.fn(() => ({ - Commands: {}, - })); - - // Create mock component - const MockRCTCheckoutWebView = (props: any) => { - return React.createElement('View', props); - }; - - return Object.setPrototypeOf( - { - requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView), - }, - RN, - ); -}); +jest.mock('../src/native/RCTCheckoutWebView'); +jest.mock('react-native'); import React from 'react'; import {render, act} from '@testing-library/react-native'; import {ShopifyCheckout} from '../src/components/ShopifyCheckout'; +import {ShopifyCheckoutSheetProvider} from '../src/context'; import {createTestCart} from './testFixtures'; +const Wrapper = ({children}: {children: React.ReactNode}) => ( + {children} +); + describe('Checkout Component - Start Events', () => { const mockCheckoutUrl = 'https://example.myshopify.com/checkout'; @@ -37,6 +23,7 @@ describe('Checkout Component - Start Events', () => { onStart={onStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -68,6 +55,7 @@ describe('Checkout Component - Start Events', () => { onStart={onStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -94,6 +82,7 @@ describe('Checkout Component - Start Events', () => { onStart={onStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -119,6 +108,7 @@ describe('Checkout Component - Start Events', () => { checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); diff --git a/modules/@shopify/checkout-sheet-kit/tests/CheckoutSubmitStart.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/CheckoutSubmitStart.test.tsx index 3dccc8e2..9f2b3b3f 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/CheckoutSubmitStart.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/CheckoutSubmitStart.test.tsx @@ -1,30 +1,16 @@ -// Mock the native view component BEFORE imports -jest.mock('react-native', () => { - const RN = jest.requireActual('react-native'); - const React = jest.requireActual('react'); - - RN.UIManager.getViewManagerConfig = jest.fn(() => ({ - Commands: {}, - })); - - // Create mock component - const MockRCTCheckoutWebView = (props: any) => { - return React.createElement('View', props); - }; - - return Object.setPrototypeOf( - { - requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView), - }, - RN, - ); -}); +jest.mock('../src/native/RCTCheckoutWebView'); +jest.mock('react-native'); import React from 'react'; import {render, act} from '@testing-library/react-native'; import {ShopifyCheckout} from '../src/components/ShopifyCheckout'; +import {ShopifyCheckoutSheetProvider} from '../src/context'; import {createTestCart} from './testFixtures'; +const Wrapper = ({children}: {children: React.ReactNode}) => ( + {children} +); + describe('Checkout Component - Submit Start Events', () => { const mockCheckoutUrl = 'https://example.myshopify.com/checkout'; @@ -37,6 +23,7 @@ describe('Checkout Component - Submit Start Events', () => { onSubmitStart={onSubmitStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -68,6 +55,7 @@ describe('Checkout Component - Submit Start Events', () => { checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -95,6 +83,7 @@ describe('Checkout Component - Submit Start Events', () => { onSubmitStart={onSubmitStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); diff --git a/modules/@shopify/checkout-sheet-kit/tests/ShopifyCheckout.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/ShopifyCheckout.test.tsx index cc602ca4..2e1a2fdf 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/ShopifyCheckout.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/ShopifyCheckout.test.tsx @@ -1,37 +1,17 @@ -// Mock the native view component BEFORE imports -jest.mock('react-native', () => { - const RN = jest.requireActual('react-native'); - const React = jest.requireActual('react'); - - RN.UIManager.getViewManagerConfig = jest.fn(() => ({ - Commands: {reload: 'reload'}, - })); - - RN.UIManager.dispatchViewManagerCommand = jest.fn(); - - RN.findNodeHandle = jest.fn(() => 123); - - // Create mock component - const MockRCTCheckoutWebView = React.forwardRef((props: any, ref: any) => { - React.useImperativeHandle(ref, () => ({})); - return React.createElement('View', props); - }); - - return Object.setPrototypeOf( - { - requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView), - }, - RN, - ); -}); +jest.mock('../src/native/RCTCheckoutWebView'); +jest.mock('react-native'); import React, {createRef} from 'react'; import {render, act} from '@testing-library/react-native'; import {ShopifyCheckout} from '../src/components/ShopifyCheckout'; import type {ShopifyCheckoutRef} from '../src/components/ShopifyCheckout'; -import {ShopifyCheckoutEventProvider} from '../src/ShopifyCheckoutEventProvider'; +import {ShopifyCheckoutSheetProvider} from '../src/context'; import {createTestCart} from './testFixtures'; +const Wrapper = ({children}: {children: React.ReactNode}) => ( + {children} +); + describe('Checkout Component', () => { const mockCheckoutUrl = 'https://example.myshopify.com/checkout'; @@ -53,6 +33,7 @@ describe('Checkout Component', () => { onCancel={onCancel} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -76,6 +57,7 @@ describe('Checkout Component', () => { onLinkClick={onLinkClick} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -99,6 +81,7 @@ describe('Checkout Component', () => { onLinkClick={onLinkClick} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -124,6 +107,7 @@ describe('Checkout Component', () => { onPaymentMethodChangeStart={onPaymentMethodChangeStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -155,6 +139,7 @@ describe('Checkout Component', () => { onPaymentMethodChangeStart={onPaymentMethodChangeStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -176,6 +161,7 @@ describe('Checkout Component', () => { onPaymentMethodChangeStart={onPaymentMethodChangeStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); @@ -207,6 +193,7 @@ describe('Checkout Component', () => { checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />, + {wrapper: Wrapper}, ); expect(ref.current).toBeDefined(); @@ -214,41 +201,6 @@ describe('Checkout Component', () => { }); }); - describe('ShopifyCheckoutEventProvider integration', () => { - it('registers webview with event provider on mount', () => { - const registerWebView = jest.fn(); - const unregisterWebView = jest.fn(); - - jest.doMock('../src/ShopifyCheckoutEventProvider', () => ({ - useCheckoutEvents: () => ({ - registerWebView, - unregisterWebView, - respondToEvent: jest.fn(), - }), - })); - - render( - - - , - ); - }); - - it('works without event provider wrapper', () => { - expect(() => { - render( - , - ); - }).not.toThrow(); - }); - }); - describe('multiple callbacks', () => { it('supports all callbacks simultaneously', () => { const onStart = jest.fn(); @@ -273,6 +225,7 @@ describe('Checkout Component', () => { onSubmitStart={onSubmitStart} testID="checkout-webview" />, + {wrapper: Wrapper}, ); const nativeComponent = getByTestId('checkout-webview'); diff --git a/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx index 636dcc0c..84e71a81 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx @@ -1,9 +1,12 @@ -import React from 'react'; -import {render, act} from '@testing-library/react-native'; -import {NativeModules, Platform} from 'react-native'; +import React, {useRef} from 'react'; +import {render, act, renderHook, screen} from '@testing-library/react-native'; +import {Text} from 'react-native'; +import {NativeModules, Platform, UIManager, findNodeHandle} from 'react-native'; import { ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet, + useShopifyEvent, + useWebviewRegistration, } from '../src/context'; import {ApplePayContactField, ColorScheme, type Configuration} from '../src'; @@ -14,18 +17,11 @@ const config: Configuration = { jest.mock('react-native'); -const HookTestComponent = ({ - onHookValue, -}: { - onHookValue: (value: any) => void; -}) => { - const hookValue = useShopifyCheckoutSheet(); - onHookValue(hookValue); - return null; +const ContextConsumer = () => { + const context = useShopifyCheckoutSheet(); + return {String(context.version)}; }; -const MockChild = () => null; - describe('ShopifyCheckoutSheetProvider', () => { const TestComponent = ({children}: {children: React.ReactNode}) => ( @@ -37,35 +33,41 @@ describe('ShopifyCheckoutSheetProvider', () => { jest.clearAllMocks(); }); - it('renders without crashing', () => { + it('renders without crashing', async () => { const component = render( - + , ); + await screen.findByTestId('context-ready'); + expect(component).toBeTruthy(); }); - it('creates ShopifyCheckoutSheet instance with configuration', () => { + it('creates ShopifyCheckoutSheet instance with configuration', async () => { render( - + , ); + await screen.findByTestId('context-ready'); + expect( NativeModules.ShopifyCheckoutSheetKit.setConfig, ).toHaveBeenCalledWith(config); }); - it('skips configuration when no configuration is provided', () => { + it('skips configuration when no configuration is provided', async () => { render( - + , ); + await screen.findByTestId('context-ready'); + expect( NativeModules.ShopifyCheckoutSheetKit.setConfig, ).not.toHaveBeenCalled(); @@ -102,13 +104,11 @@ describe('ShopifyCheckoutSheetProvider', () => { render( - + , ); - await act(async () => { - await Promise.resolve(); - }); + await screen.findByTestId('context-ready'); expect( NativeModules.ShopifyCheckoutSheetKit.configureAcceleratedCheckouts, @@ -123,19 +123,23 @@ describe('ShopifyCheckoutSheetProvider', () => { ); }); - it('reuses the same instance across re-renders', () => { + it('reuses the same instance across re-renders', async () => { const {rerender} = render( - + , ); + await screen.findByTestId('context-ready'); + rerender( - + , ); + await screen.findByTestId('context-ready'); + expect( NativeModules.ShopifyCheckoutSheetKit.setConfig.mock.calls, ).toHaveLength(2); @@ -154,54 +158,27 @@ describe('useShopifyCheckoutSheet', () => { }); it('provides addEventListener function', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); - render( - - - , - ); - - expect(hookValue.addEventListener).toBeDefined(); - expect(typeof hookValue.addEventListener).toBe('function'); + expect(result.current.addEventListener).toBeDefined(); + expect(typeof result.current.addEventListener).toBe('function'); }); it('provides removeEventListeners function', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.removeEventListeners('close'); + result.current.removeEventListeners('close'); }); - expect(hookValue.removeEventListeners).toBeDefined(); + expect(result.current.removeEventListeners).toBeDefined(); }); it('provides present function and calls it with checkoutUrl', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.present(checkoutUrl); + result.current.present(checkoutUrl); }); expect(NativeModules.ShopifyCheckoutSheetKit.present).toHaveBeenCalledWith( @@ -211,19 +188,10 @@ describe('useShopifyCheckoutSheet', () => { }); it('does not call present with empty checkoutUrl', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.present(''); + result.current.present(''); }); expect( @@ -232,19 +200,10 @@ describe('useShopifyCheckoutSheet', () => { }); it('provides preload function and calls it with checkoutUrl', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.preload(checkoutUrl); + result.current.preload(checkoutUrl); }); expect(NativeModules.ShopifyCheckoutSheetKit.preload).toHaveBeenCalledWith( @@ -254,19 +213,10 @@ describe('useShopifyCheckoutSheet', () => { }); it('does not call preload with empty checkoutUrl', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.preload(''); + result.current.preload(''); }); expect( @@ -275,19 +225,10 @@ describe('useShopifyCheckoutSheet', () => { }); it('provides invalidate function', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.invalidate(); + result.current.invalidate(); }); expect( @@ -296,40 +237,22 @@ describe('useShopifyCheckoutSheet', () => { }); it('provides dismiss function', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); act(() => { - hookValue.dismiss(); + result.current.dismiss(); }); expect(NativeModules.ShopifyCheckoutSheetKit.dismiss).toHaveBeenCalled(); }); it('provides setConfig function', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); const newConfig = {colorScheme: ColorScheme.light}; - render( - - - , - ); - act(() => { - hookValue.setConfig(newConfig); + result.current.setConfig(newConfig); }); expect( @@ -338,19 +261,10 @@ describe('useShopifyCheckoutSheet', () => { }); it('provides getConfig function', async () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); await act(async () => { - const config = await hookValue.getConfig(); + const config = await result.current.getConfig(); expect(config).toEqual({preloading: true}); }); @@ -358,35 +272,17 @@ describe('useShopifyCheckoutSheet', () => { }); it('provides version from the instance', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); - render( - - - , - ); - - expect(hookValue.version).toBe('0.7.0'); + expect(result.current.version).toBe('0.7.0'); }); it('addEventListener returns subscription object', () => { - let hookValue: any; - const onHookValue = (value: any) => { - hookValue = value; - }; - - render( - - - , - ); + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); - const subscription = hookValue.addEventListener('close', jest.fn()); + const subscription = result.current.addEventListener('close', jest.fn()); expect(subscription).toBeDefined(); - expect(subscription.remove).toBeDefined(); + expect(subscription?.remove).toBeDefined(); }); }); @@ -395,8 +291,118 @@ describe('ShopifyCheckoutSheetContext without provider', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(); expect(() => { - render( {}} />); - }).toThrow('useShopifyCheckoutSheet must be used from within a ShopifyCheckoutSheetContext'); + renderHook(() => useShopifyCheckoutSheet()); + }).toThrow( + 'useShopifyCheckoutSheet must be used from within a ShopifyCheckoutSheetContext', + ); + + errorSpy.mockRestore(); + }); +}); + +describe('useWebview behavior (via useShopifyCheckoutSheet)', () => { + const Wrapper = ({children}: {children: React.ReactNode}) => ( + + {children} + + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('respondToEvent returns false when no webview is registered', async () => { + const {result} = renderHook(() => useShopifyCheckoutSheet(), {wrapper: Wrapper}); + + let returnValue: boolean = true; + await act(async () => { + returnValue = await result.current.respondToEvent('event-123', {foo: 'bar'}); + }); + + expect(returnValue).toBe(false); + expect(UIManager.dispatchViewManagerCommand).not.toHaveBeenCalled(); + }); + + it('respondToEvent dispatches native command when webview is registered', async () => { + const {result} = renderHook(() => { + const hookValue = useShopifyCheckoutSheet(); + const webViewRef = useRef({current: {}}); + useWebviewRegistration(webViewRef); + return hookValue; + }, {wrapper: Wrapper}); + + let returnValue: boolean = false; + await act(async () => { + returnValue = await result.current.respondToEvent('event-123', {foo: 'bar'}); + }); + + expect(returnValue).toBe(true); + expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledWith( + 1, + 'respondToEvent', + ['event-123', JSON.stringify({foo: 'bar'})], + ); + }); + + it('respondToEvent returns false when findNodeHandle returns null', async () => { + (findNodeHandle as jest.Mock).mockReturnValueOnce(null); + + const {result} = renderHook(() => { + const hookValue = useShopifyCheckoutSheet(); + const webViewRef = useRef({current: {}}); + useWebviewRegistration(webViewRef); + return hookValue; + }, {wrapper: Wrapper}); + + let returnValue: boolean = true; + await act(async () => { + returnValue = await result.current.respondToEvent('event-123', {foo: 'bar'}); + }); + + expect(returnValue).toBe(false); + expect(UIManager.dispatchViewManagerCommand).not.toHaveBeenCalled(); + }); +}); + +describe('useShopifyEvent', () => { + const Wrapper = ({children}: {children: React.ReactNode}) => ( + + {children} + + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('respondWith delegates to context.respondToEvent with correct eventId', async () => { + const {result} = renderHook(() => { + const webViewRef = useRef({current: {}}); + useWebviewRegistration(webViewRef); + return useShopifyEvent('test-event-456'); + }, {wrapper: Wrapper}); + + expect(result.current.id).toBe('test-event-456'); + + let returnValue: boolean = false; + await act(async () => { + returnValue = await result.current.respondWith({payment: 'data'}); + }); + + expect(returnValue).toBe(true); + expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledWith( + 1, + 'respondToEvent', + ['test-event-456', JSON.stringify({payment: 'data'})], + ); + }); + + it('throws error when used outside provider', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(); + + expect(() => { + renderHook(() => useShopifyEvent('test-event-789')); + }).toThrow('useShopifyEvent must be used within ShopifyCheckoutSheetProvider'); errorSpy.mockRestore(); }); diff --git a/sample/.prettierignore b/sample/.prettierignore index 3a13d2fa..9044214d 100644 --- a/sample/.prettierignore +++ b/sample/.prettierignore @@ -1,4 +1,5 @@ -ios/Pods/ +ios/ +android/ vendor/ node_modules/ android/build/ diff --git a/sample/src/screens/BuyNow/CheckoutScreen.tsx b/sample/src/screens/BuyNow/CheckoutScreen.tsx index a6a9b14d..6b3daa2a 100644 --- a/sample/src/screens/BuyNow/CheckoutScreen.tsx +++ b/sample/src/screens/BuyNow/CheckoutScreen.tsx @@ -28,7 +28,7 @@ import { type ShopifyCheckoutRef, type CheckoutStartEvent, type CheckoutSubmitStartEvent, - useCheckoutEvents, + useShopifyCheckoutSheet, } from '@shopify/checkout-sheet-kit'; import type {BuyNowStackParamList} from './types'; import {StyleSheet} from 'react-native'; @@ -40,7 +40,7 @@ export default function CheckoutScreen(props: { }) { const navigation = useNavigation>(); const ref = useRef(null); - const eventContext = useCheckoutEvents(); + const {respondToEvent} = useShopifyCheckoutSheet(); const onStart = (event: CheckoutStartEvent) => { console.log(' onStart', event); @@ -61,7 +61,7 @@ export default function CheckoutScreen(props: { const onSubmitStart = async (event: CheckoutSubmitStartEvent) => { console.log(' onSubmitStart', event); try { - await eventContext?.respondToEvent(event.id, { + await respondToEvent(event.id, { cart: { ...event.cart, payment: { diff --git a/sample/src/screens/BuyNow/index.tsx b/sample/src/screens/BuyNow/index.tsx index 053a552d..1cc31e0f 100644 --- a/sample/src/screens/BuyNow/index.tsx +++ b/sample/src/screens/BuyNow/index.tsx @@ -21,7 +21,6 @@ import {createNativeStackNavigator} from '@react-navigation/native-stack'; import type {RouteProp} from '@react-navigation/native'; import React from 'react'; import {Button} from 'react-native'; -import {ShopifyCheckoutEventProvider} from '@shopify/checkout-sheet-kit'; import type {RootStackParamList} from '../../App'; import CheckoutScreen from './CheckoutScreen'; import AddressScreen from './AddressScreen'; @@ -40,41 +39,39 @@ type BuyNowStackProps = { export default function BuyNowStack(props: BuyNowStackProps) { const {colors} = useTheme(); return ( - - ({ - headerStyle: {backgroundColor: colors.webviewHeaderBackgroundColor}, - headerTintColor: colors.webviewHeaderTextColor, - // eslint-disable-next-line react/no-unstable-nested-components - headerRight: () => ( -