Skip to content

Commit b425007

Browse files
refactor: reuse common mock for native module - simplify hooks
1 parent 5185569 commit b425007

File tree

10 files changed

+173
-288
lines changed

10 files changed

+173
-288
lines changed

modules/@shopify/checkout-sheet-kit/src/components/ShopifyCheckout.tsx

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,21 @@ import React, {
2222
useCallback,
2323
useImperativeHandle,
2424
forwardRef,
25-
useEffect,
2625
} from 'react';
26+
import {UIManager, findNodeHandle, type ViewStyle} from 'react-native';
27+
import {useWebviewRegistration} from '../context';
2728
import {
28-
requireNativeComponent,
29-
UIManager,
30-
findNodeHandle,
31-
type ViewStyle,
32-
} from 'react-native';
33-
import {useShopifyCheckoutSheet} from '../context';
29+
RCTCheckoutWebView,
30+
type NativeShopifyCheckoutWebViewProps,
31+
} from '../native/RCTCheckoutWebView';
3432
import type {
3533
CheckoutAddressChangeStartEvent,
3634
CheckoutCompleteEvent,
3735
CheckoutPaymentMethodChangeStartEvent,
3836
CheckoutStartEvent,
3937
CheckoutSubmitStartEvent,
4038
} from '../events.d';
41-
import {
42-
parseCheckoutError,
43-
type CheckoutException,
44-
type CheckoutNativeError,
45-
} from '../errors.d';
39+
import {parseCheckoutError, type CheckoutException} from '../errors.d';
4640

4741
export interface ShopifyCheckoutProps {
4842
/**
@@ -123,30 +117,6 @@ export interface ShopifyCheckoutRef {
123117
reload: () => void;
124118
}
125119

126-
interface NativeShopifyCheckoutWebViewProps {
127-
checkoutUrl: string;
128-
auth?: string;
129-
style?: ViewStyle;
130-
testID?: string;
131-
onStart?: (event: {nativeEvent: CheckoutStartEvent}) => void;
132-
onFail?: (event: {nativeEvent: CheckoutNativeError}) => void;
133-
onComplete?: (event: {nativeEvent: CheckoutCompleteEvent}) => void;
134-
onCancel?: () => void;
135-
onLinkClick?: (event: {nativeEvent: {url: string}}) => void;
136-
onAddressChangeStart?: (event: {
137-
nativeEvent: CheckoutAddressChangeStartEvent;
138-
}) => void;
139-
onSubmitStart?: (event: {nativeEvent: CheckoutSubmitStartEvent}) => void;
140-
onPaymentMethodChangeStart?: (event: {
141-
nativeEvent: CheckoutPaymentMethodChangeStartEvent;
142-
}) => void;
143-
}
144-
145-
const RCTCheckoutWebView =
146-
requireNativeComponent<NativeShopifyCheckoutWebViewProps>(
147-
'RCTCheckoutWebView',
148-
);
149-
150120
/**
151121
* Checkout provides a native webview component for displaying
152122
* Shopify checkout pages directly within your React Native app.
@@ -211,12 +181,7 @@ export const ShopifyCheckout = forwardRef<
211181
) => {
212182
const webViewRef =
213183
useRef<React.ComponentRef<typeof RCTCheckoutWebView>>(null);
214-
const {registerWebView, unregisterWebView} = useShopifyCheckoutSheet();
215-
216-
useEffect(() => {
217-
registerWebView(webViewRef);
218-
return () => unregisterWebView();
219-
}, [registerWebView, unregisterWebView]);
184+
useWebviewRegistration(webViewRef);
220185

221186
const handleStart = useCallback<
222187
Required<NativeShopifyCheckoutWebViewProps>['onStart']

modules/@shopify/checkout-sheet-kit/src/context.tsx

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2323

2424
import React, {useCallback, useMemo, useRef, useEffect, useState} from 'react';
2525
import type {PropsWithChildren} from 'react';
26-
import {type EmitterSubscription, UIManager, findNodeHandle} from 'react-native';
26+
import {
27+
type EmitterSubscription,
28+
UIManager,
29+
findNodeHandle,
30+
} from 'react-native';
2731
import {ShopifyCheckoutSheet} from './index';
2832
import type {Features} from './index.d';
2933
import type {
@@ -34,13 +38,34 @@ import type {
3438
CheckoutOptions,
3539
} from './index.d';
3640

37-
interface WebviewContextType {
41+
type Maybe<T> = T | undefined;
42+
43+
interface Context {
44+
acceleratedCheckoutsAvailable: boolean;
45+
addEventListener: AddEventListener;
46+
getConfig: () => Promise<Configuration | undefined>;
47+
setConfig: (config: Configuration) => Promise<void>;
48+
removeEventListeners: RemoveEventListeners;
49+
preload: (checkoutUrl: string, options?: CheckoutOptions) => void;
50+
present: (checkoutUrl: string, options?: CheckoutOptions) => void;
51+
dismiss: () => void;
52+
invalidate: () => void;
53+
version: Maybe<string>;
54+
respondToEvent: (eventId: string, response: any) => Promise<boolean>;
55+
}
56+
57+
interface InternalContext extends Context {
58+
registerWebView: (ref: React.RefObject<any>) => void;
59+
unregisterWebView: () => void;
60+
}
61+
62+
interface WebviewState {
3863
registerWebView: (ref: React.RefObject<any>) => void;
3964
unregisterWebView: () => void;
4065
respondToEvent: (eventId: string, response: any) => Promise<boolean>;
4166
}
4267

43-
function useWebview(): WebviewContextType {
68+
function useWebview(): WebviewState {
4469
const webViewRef = useRef<React.RefObject<any> | null>(null);
4570

4671
const registerWebView = useCallback((ref: React.RefObject<any>) => {
@@ -79,23 +104,8 @@ function useWebview(): WebviewContextType {
79104
return {registerWebView, unregisterWebView, respondToEvent};
80105
}
81106

82-
type Maybe<T> = T | undefined;
83-
84-
interface Context extends WebviewContextType {
85-
acceleratedCheckoutsAvailable: boolean;
86-
addEventListener: AddEventListener;
87-
getConfig: () => Promise<Configuration | undefined>;
88-
setConfig: (config: Configuration) => Promise<void>;
89-
removeEventListeners: RemoveEventListeners;
90-
preload: (checkoutUrl: string, options?: CheckoutOptions) => void;
91-
present: (checkoutUrl: string, options?: CheckoutOptions) => void;
92-
dismiss: () => void;
93-
invalidate: () => void;
94-
version: Maybe<string>;
95-
}
96-
97-
const ShopifyCheckoutSheetContext = React.createContext<Context>(
98-
null as unknown as Context,
107+
const ShopifyCheckoutSheetContext = React.createContext<InternalContext>(
108+
null as unknown as InternalContext,
99109
);
100110

101111
interface Props {
@@ -124,7 +134,8 @@ export function ShopifyCheckoutSheetProvider({
124134
}
125135

126136
await instance.current?.setConfig(configuration);
127-
const isAvailable = await instance.current.isAcceleratedCheckoutAvailable();
137+
const isAvailable =
138+
await instance.current.isAcceleratedCheckoutAvailable();
128139
setAcceleratedCheckoutsAvailable(isAvailable);
129140
}
130141

@@ -142,18 +153,23 @@ export function ShopifyCheckoutSheetProvider({
142153
instance.current?.removeEventListeners(eventName);
143154
}, []);
144155

145-
const present = useCallback((checkoutUrl: string, options?: CheckoutOptions) => {
146-
if (checkoutUrl) {
147-
instance.current?.present(checkoutUrl, options
148-
);
149-
}
150-
}, []);
156+
const present = useCallback(
157+
(checkoutUrl: string, options?: CheckoutOptions) => {
158+
if (checkoutUrl) {
159+
instance.current?.present(checkoutUrl, options);
160+
}
161+
},
162+
[],
163+
);
151164

152-
const preload = useCallback((checkoutUrl: string, options?: CheckoutOptions) => {
153-
if (checkoutUrl) {
154-
instance.current?.preload(checkoutUrl, options);
155-
}
156-
}, []);
165+
const preload = useCallback(
166+
(checkoutUrl: string, options?: CheckoutOptions) => {
167+
if (checkoutUrl) {
168+
instance.current?.preload(checkoutUrl, options);
169+
}
170+
},
171+
[],
172+
);
157173

158174
const invalidate = useCallback(() => {
159175
instance.current?.invalidate();
@@ -171,7 +187,7 @@ export function ShopifyCheckoutSheetProvider({
171187
return instance.current?.getConfig();
172188
}, []);
173189

174-
const context = useMemo((): Context => {
190+
const context = useMemo((): InternalContext => {
175191
return {
176192
acceleratedCheckoutsAvailable,
177193
addEventListener,
@@ -209,14 +225,31 @@ export function ShopifyCheckoutSheetProvider({
209225
);
210226
}
211227

212-
export function useShopifyCheckoutSheet() {
228+
export function useShopifyCheckoutSheet(): Context {
213229
const context = React.useContext(ShopifyCheckoutSheetContext);
214230
if (!context) {
215231
throw new Error(
216232
'useShopifyCheckoutSheet must be used from within a ShopifyCheckoutSheetContext',
217233
);
218234
}
219-
return context;
235+
return useMemo(() => {
236+
const {registerWebView, unregisterWebView, ...publicContext} = context;
237+
return publicContext;
238+
}, [context]);
239+
}
240+
241+
export function useWebviewRegistration(webViewRef: React.RefObject<any>) {
242+
const context = React.useContext(ShopifyCheckoutSheetContext);
243+
if (!context) {
244+
throw new Error(
245+
'useWebviewRegistration must be used within ShopifyCheckoutSheetProvider',
246+
);
247+
}
248+
249+
useEffect(() => {
250+
context.registerWebView(webViewRef);
251+
return () => context.unregisterWebView();
252+
}, [context, webViewRef]);
220253
}
221254

222255
export function useShopifyEvent(eventId: string) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {requireNativeComponent} from 'react-native';
2+
import type {ViewStyle} from 'react-native';
3+
import type {
4+
CheckoutStartEvent,
5+
CheckoutCompleteEvent,
6+
CheckoutAddressChangeStartEvent,
7+
CheckoutSubmitStartEvent,
8+
CheckoutPaymentMethodChangeStartEvent,
9+
} from '../events.d';
10+
import type {CheckoutNativeError} from '../errors.d';
11+
12+
export interface NativeShopifyCheckoutWebViewProps {
13+
checkoutUrl: string;
14+
auth?: string;
15+
style?: ViewStyle;
16+
testID?: string;
17+
onStart?: (event: {nativeEvent: CheckoutStartEvent}) => void;
18+
onFail?: (event: {nativeEvent: CheckoutNativeError}) => void;
19+
onComplete?: (event: {nativeEvent: CheckoutCompleteEvent}) => void;
20+
onCancel?: () => void;
21+
onLinkClick?: (event: {nativeEvent: {url: string}}) => void;
22+
onAddressChangeStart?: (event: {
23+
nativeEvent: CheckoutAddressChangeStartEvent;
24+
}) => void;
25+
onSubmitStart?: (event: {nativeEvent: CheckoutSubmitStartEvent}) => void;
26+
onPaymentMethodChangeStart?: (event: {
27+
nativeEvent: CheckoutPaymentMethodChangeStartEvent;
28+
}) => void;
29+
}
30+
31+
export const RCTCheckoutWebView =
32+
requireNativeComponent<NativeShopifyCheckoutWebViewProps>(
33+
'RCTCheckoutWebView',
34+
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import React from 'react';
2+
3+
export const RCTCheckoutWebView = (props: any) =>
4+
React.createElement('View', props);

modules/@shopify/checkout-sheet-kit/tests/CheckoutAddressChange.test.tsx

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,5 @@
1-
// Mock the native view component BEFORE imports
2-
jest.mock('react-native', () => {
3-
const RN = jest.requireActual('react-native');
4-
const React = jest.requireActual('react');
5-
6-
RN.UIManager.getViewManagerConfig = jest.fn(() => ({
7-
Commands: {},
8-
}));
9-
10-
RN.NativeModules.ShopifyCheckoutSheetKit = {
11-
version: '0.7.0',
12-
preload: jest.fn(),
13-
present: jest.fn(),
14-
dismiss: jest.fn(),
15-
invalidateCache: jest.fn(),
16-
getConfig: jest.fn(async () => ({preloading: true})),
17-
setConfig: jest.fn(),
18-
addEventListener: jest.fn(),
19-
removeEventListeners: jest.fn(),
20-
initiateGeolocationRequest: jest.fn(),
21-
configureAcceleratedCheckouts: jest.fn(),
22-
isAcceleratedCheckoutAvailable: jest.fn(),
23-
};
24-
25-
// Create mock component
26-
const MockRCTCheckoutWebView = (props: any) => {
27-
return React.createElement('View', props);
28-
};
29-
30-
return Object.setPrototypeOf(
31-
{
32-
requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView),
33-
},
34-
RN,
35-
);
36-
});
1+
jest.mock('../src/native/RCTCheckoutWebView');
2+
jest.mock('react-native');
373

384
import React from 'react';
395
import {render, act} from '@testing-library/react-native';
@@ -86,7 +52,10 @@ describe('Checkout Component - Address Change Events', () => {
8652

8753
it('does not crash when onAddressChangeStart prop is not provided', () => {
8854
const {getByTestId} = render(
89-
<ShopifyCheckout checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />,
55+
<ShopifyCheckout
56+
checkoutUrl={mockCheckoutUrl}
57+
testID="checkout-webview"
58+
/>,
9059
{wrapper: Wrapper},
9160
);
9261

modules/@shopify/checkout-sheet-kit/tests/CheckoutComplete.test.tsx

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,5 @@
1-
// Mock the native view component BEFORE imports
2-
jest.mock('react-native', () => {
3-
const RN = jest.requireActual('react-native');
4-
const React = jest.requireActual('react');
5-
6-
RN.UIManager.getViewManagerConfig = jest.fn(() => ({
7-
Commands: {},
8-
}));
9-
10-
RN.NativeModules.ShopifyCheckoutSheetKit = {
11-
version: '0.7.0',
12-
preload: jest.fn(),
13-
present: jest.fn(),
14-
dismiss: jest.fn(),
15-
invalidateCache: jest.fn(),
16-
getConfig: jest.fn(async () => ({preloading: true})),
17-
setConfig: jest.fn(),
18-
addEventListener: jest.fn(),
19-
removeEventListeners: jest.fn(),
20-
initiateGeolocationRequest: jest.fn(),
21-
configureAcceleratedCheckouts: jest.fn(),
22-
isAcceleratedCheckoutAvailable: jest.fn(),
23-
};
24-
25-
// Create mock component
26-
const MockRCTCheckoutWebView = (props: any) => {
27-
return React.createElement('View', props);
28-
};
29-
30-
return Object.setPrototypeOf(
31-
{
32-
requireNativeComponent: jest.fn(() => MockRCTCheckoutWebView),
33-
},
34-
RN,
35-
);
36-
});
1+
jest.mock('../src/native/RCTCheckoutWebView');
2+
jest.mock('react-native');
373

384
import React from 'react';
395
import {render, act} from '@testing-library/react-native';
@@ -185,7 +151,10 @@ describe('Checkout Component - Complete Events', () => {
185151

186152
it('does not crash when onComplete prop is not provided', () => {
187153
const {getByTestId} = render(
188-
<ShopifyCheckout checkoutUrl={mockCheckoutUrl} testID="checkout-webview" />,
154+
<ShopifyCheckout
155+
checkoutUrl={mockCheckoutUrl}
156+
testID="checkout-webview"
157+
/>,
189158
{wrapper: Wrapper},
190159
);
191160

0 commit comments

Comments
 (0)