Skip to content

Commit b0baa2d

Browse files
Add TurboModule specs and codegen components to prepare RN new-arch migration
Prepare codebase for React Native new-architecture (TurboModules/codegen) by adding JS specs, codegen native components, and test mocks. What changed and why: - Added a TurboModule spec (NativeShopifyCheckoutSheetKit) and a codegen native component spec (RCTAcceleratedCheckoutButtonsNativeComponent) to declare the native API surface and typed component props. This moves JS to consume the new-arch API surface via TurboModuleRegistry and codegenNativeComponent instead of using NativeModules/requireNativeComponent directly. - Updated AcceleratedCheckoutButtons to import codegenNativeComponent and use the generated native component type instead of requireNativeComponent. - Added codegenConfig to the package.json to configure codegen generation for this module. - Added Jest mocks for codegenNativeComponent and extended the react-native manual mock to provide TurboModuleRegistry.getEnforcing, getConstants, and event listener helpers so unit tests run against the new-arch surface. - Added a moduleNameMapper entry to jest.config.js so tests resolve the codegenNativeComponent import to the new mock. - Updated a linking test to assert TurboModuleRegistry behavior (throws when module not present) rather than the old NativeModules linking error. - Adjusted runtime usage to read version via getConstants() and cast getConfig() return types for better typing with the spec. Context and next steps: - These changes are groundwork only: they enable the JS-side migration to the RN new architecture and allow tests to run against the new-arch signatures. - Native implementations (iOS/Android) still need corresponding TurboModule and codegen native component implementations, and we must decide on compatibility strategy (clean break vs dual support). Dual support will add duplication (parallel native hooks, bridging shims) and phased rollout; a clean break requires coordinating release timelines for hosts consuming this module. - Suggested next phases: (1) implement native TurboModule & codegen components on each platform, (2) add migration shims to continue supporting old NativeModules if dual-mode required, (3) integration testing & performance validation, (4) cutover and remove old-arch shims when safe. This commit prepares the repository for the migration and makes future work (native bindings and compatibility strategy) easier to implement and test.
1 parent 9e4ccf3 commit b0baa2d

9 files changed

Lines changed: 199 additions & 36 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const React = require('react');
2+
3+
const codegenNativeComponent = (_name: string) => {
4+
return (props: any) =>
5+
React.createElement('View', {
6+
...props,
7+
testID: props?.testID ?? 'accelerated-checkout-buttons',
8+
});
9+
};
10+
11+
export default codegenNativeComponent;

__mocks__/react-native.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const exampleConfig = {preloading: true};
4747

4848
const ShopifyCheckoutSheetKit = {
4949
version: '0.7.0',
50+
getConstants: jest.fn(() => ({version: '0.7.0'})),
5051
preload: jest.fn(),
5152
present: jest.fn(),
5253
dismiss: jest.fn(),
@@ -58,6 +59,8 @@ const ShopifyCheckoutSheetKit = {
5859
initiateGeolocationRequest: jest.fn(),
5960
configureAcceleratedCheckouts: jest.fn(),
6061
isAcceleratedCheckoutAvailable: jest.fn(),
62+
addListener: jest.fn(),
63+
removeListeners: jest.fn(),
6164
};
6265

6366
// CommonJS export for Jest manual mock resolution
@@ -68,6 +71,14 @@ module.exports = {
6871
},
6972
NativeEventEmitter: jest.fn(() => createMockEmitter()),
7073
requireNativeComponent,
74+
TurboModuleRegistry: {
75+
getEnforcing: jest.fn((name: string) => {
76+
if (name === 'ShopifyCheckoutSheetKit') {
77+
return ShopifyCheckoutSheetKit;
78+
}
79+
return null;
80+
}),
81+
},
7182
NativeModules: {
7283
ShopifyCheckoutSheetKit: {
7384
...ShopifyCheckoutSheetKit,

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ module.exports = {
33
modulePathIgnorePatterns: ['modules/@shopify/checkout-sheet-kit/lib'],
44
modulePaths: ['<rootDir>/sample/node_modules'],
55
setupFiles: ['<rootDir>/jest.setup.ts'],
6+
moduleNameMapper: {
7+
'react-native/Libraries/Utilities/codegenNativeComponent':
8+
'<rootDir>/__mocks__/codegenNativeComponent.ts',
9+
},
610
transform: {
711
'\\.[jt]sx?$': 'babel-jest',
812
},

modules/@shopify/checkout-sheet-kit/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@
5454
"react-native-builder-bob": "^0.23.2",
5555
"typescript": "^5.9.2"
5656
},
57+
"codegenConfig": {
58+
"name": "RNShopifyCheckoutSheetKitSpec",
59+
"type": "all",
60+
"jsSrcsDir": "src/specs",
61+
"android": {
62+
"javaPackageName": "com.shopify.checkoutsheetkit"
63+
}
64+
},
5765
"react-native-builder-bob": {
5866
"source": "src",
5967
"output": "lib",

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO
2222
*/
2323

2424
import React, {useCallback, useMemo, useState} from 'react';
25-
import {requireNativeComponent, Platform} from 'react-native';
25+
import {Platform} from 'react-native';
2626
import type {ViewStyle} from 'react-native';
27+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
2728
import type {
2829
AcceleratedCheckoutWallet,
2930
CheckoutCompletedEvent,
@@ -164,7 +165,7 @@ interface NativeAcceleratedCheckoutButtonsProps {
164165
}
165166

166167
const RCTAcceleratedCheckoutButtons =
167-
requireNativeComponent<NativeAcceleratedCheckoutButtonsProps>(
168+
codegenNativeComponent<NativeAcceleratedCheckoutButtonsProps>(
168169
'RCTAcceleratedCheckoutButtons',
169170
);
170171

modules/@shopify/checkout-sheet-kit/src/index.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2121
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2222
*/
2323

24-
import {
25-
NativeModules,
26-
NativeEventEmitter,
27-
PermissionsAndroid,
28-
Platform,
29-
} from 'react-native';
24+
import {NativeEventEmitter, PermissionsAndroid, Platform} from 'react-native';
3025
import type {
3126
EmitterSubscription,
3227
EventSubscription,
3328
PermissionStatus,
3429
} from 'react-native';
30+
import NativeShopifyCheckoutSheetKit from './specs/NativeShopifyCheckoutSheetKit';
3531
import {ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet} from './context';
3632
import {ApplePayContactField, ColorScheme, LogLevel} from './index.d';
3733
import type {
@@ -64,14 +60,7 @@ import type {
6460
RenderStateChangeEvent,
6561
} from './components/AcceleratedCheckoutButtons';
6662

67-
const RNShopifyCheckoutSheetKit = NativeModules.ShopifyCheckoutSheetKit;
68-
69-
if (!('ShopifyCheckoutSheetKit' in NativeModules)) {
70-
throw new Error(`
71-
"@shopify/checkout-sheet-kit" is not correctly linked.
72-
73-
If you are building for iOS, make sure to run "pod install" first and restart the metro server.`);
74-
}
63+
const RNShopifyCheckoutSheetKit = NativeShopifyCheckoutSheetKit;
7564

7665
const defaultFeatures: Features = {
7766
handleGeolocationRequests: true,
@@ -114,7 +103,8 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit {
114103
}
115104
}
116105

117-
public readonly version: string = RNShopifyCheckoutSheetKit.version;
106+
public readonly version: string =
107+
RNShopifyCheckoutSheetKit.getConstants().version;
118108

119109
/**
120110
* Dismisses the currently displayed checkout sheet
@@ -151,7 +141,7 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit {
151141
* @returns Promise containing the current Configuration
152142
*/
153143
public async getConfig(): Promise<Configuration> {
154-
return RNShopifyCheckoutSheetKit.getConfig();
144+
return RNShopifyCheckoutSheetKit.getConfig() as Promise<Configuration>;
155145
}
156146

157147
/**
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type {TurboModule} from 'react-native';
2+
import {TurboModuleRegistry} from 'react-native';
3+
4+
type IosColorsSpec = {
5+
tintColor?: string;
6+
backgroundColor?: string;
7+
closeButtonColor?: string;
8+
};
9+
10+
type AndroidColorsBaseSpec = {
11+
progressIndicator?: string;
12+
backgroundColor?: string;
13+
headerBackgroundColor?: string;
14+
headerTextColor?: string;
15+
closeButtonColor?: string;
16+
};
17+
18+
type AndroidColorsSpec = {
19+
progressIndicator?: string;
20+
backgroundColor?: string;
21+
headerBackgroundColor?: string;
22+
headerTextColor?: string;
23+
closeButtonColor?: string;
24+
light?: AndroidColorsBaseSpec;
25+
dark?: AndroidColorsBaseSpec;
26+
};
27+
28+
type ColorsSpec = {
29+
ios?: IosColorsSpec;
30+
android?: AndroidColorsSpec;
31+
};
32+
33+
type ConfigurationSpec = {
34+
preloading?: boolean;
35+
title?: string;
36+
colorScheme?: string;
37+
logLevel?: string;
38+
colors?: ColorsSpec;
39+
};
40+
41+
type ConfigurationResultSpec = {
42+
preloading: boolean;
43+
colorScheme: string;
44+
logLevel: string;
45+
title?: string;
46+
tintColor?: string;
47+
backgroundColor?: string;
48+
closeButtonColor?: string;
49+
};
50+
51+
export interface Spec extends TurboModule {
52+
present(checkoutUrl: string): void;
53+
preload(checkoutUrl: string): void;
54+
dismiss(): void;
55+
invalidateCache(): void;
56+
setConfig(configuration: ConfigurationSpec): void;
57+
getConfig(): Promise<ConfigurationResultSpec>;
58+
configureAcceleratedCheckouts(
59+
storefrontDomain: string,
60+
storefrontAccessToken: string,
61+
customerEmail: string | null,
62+
customerPhoneNumber: string | null,
63+
customerAccessToken: string | null,
64+
applePayMerchantIdentifier: string | null,
65+
applyPayContactFields: string[],
66+
supportedShippingCountries: string[],
67+
): Promise<boolean>;
68+
isAcceleratedCheckoutAvailable(): Promise<boolean>;
69+
isApplePayAvailable(): Promise<boolean>;
70+
initiateGeolocationRequest(allow: boolean): void;
71+
addListener(eventName: string): void;
72+
removeListeners(count: number): void;
73+
getConstants(): {version: string};
74+
}
75+
76+
export default TurboModuleRegistry.getEnforcing<Spec>(
77+
'ShopifyCheckoutSheetKit',
78+
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type {ViewProps} from 'react-native';
2+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
3+
import type {
4+
BubblingEventHandler,
5+
DirectEventHandler,
6+
Double,
7+
Float,
8+
} from 'react-native/Libraries/Types/CodegenTypes';
9+
10+
type FailEvent = Readonly<{
11+
__typename: string;
12+
message: string;
13+
code?: string;
14+
recoverable?: boolean;
15+
}>;
16+
17+
type CompleteEvent = Readonly<{
18+
orderDetails: {
19+
id: string;
20+
cart: {
21+
token: string;
22+
lines: ReadonlyArray<
23+
Readonly<{
24+
title: string;
25+
quantity: Double;
26+
merchandiseId?: string;
27+
productId?: string;
28+
}>
29+
>;
30+
};
31+
email?: string;
32+
phone?: string;
33+
};
34+
}>;
35+
36+
type RenderStateChangeEvent = Readonly<{
37+
state: string;
38+
reason?: string;
39+
}>;
40+
41+
type ClickLinkEvent = Readonly<{url: string}>;
42+
type SizeChangeEvent = Readonly<{height: Double}>;
43+
44+
type CheckoutIdentifierSpec = Readonly<{
45+
cartId?: string;
46+
variantId?: string;
47+
quantity?: Double;
48+
}>;
49+
50+
interface NativeProps extends ViewProps {
51+
checkoutIdentifier: CheckoutIdentifierSpec;
52+
cornerRadius?: Float;
53+
wallets?: ReadonlyArray<string>;
54+
applePayLabel?: string;
55+
onFail?: BubblingEventHandler<FailEvent>;
56+
onComplete?: BubblingEventHandler<CompleteEvent>;
57+
onCancel?: BubblingEventHandler<null>;
58+
onRenderStateChange?: BubblingEventHandler<RenderStateChangeEvent>;
59+
onWebPixelEvent?: BubblingEventHandler<Readonly<{data: string}>>;
60+
onClickLink?: BubblingEventHandler<ClickLinkEvent>;
61+
onSizeChange?: DirectEventHandler<SizeChangeEvent>;
62+
onShouldRecoverFromError?: DirectEventHandler<
63+
Readonly<{recoverable: boolean}>
64+
>;
65+
}
66+
67+
export default codegenNativeComponent<NativeProps>(
68+
'RCTAcceleratedCheckoutButtons',
69+
);
Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
1-
/**
2-
* Test for native module linking error
3-
*/
4-
5-
// Mock NativeModules without ShopifyCheckoutSheetKit
61
jest.mock('react-native', () => ({
7-
NativeModules: {
8-
// Intentionally empty to trigger linking error
9-
},
2+
NativeModules: {},
103
NativeEventEmitter: jest.fn(),
114
Platform: {
125
OS: 'ios',
136
},
14-
requireNativeComponent: jest.fn().mockImplementation(() => {
15-
const mockComponent = (props: any) => {
16-
// Use React.createElement with plain object instead
17-
const mockReact = jest.requireActual('react');
18-
return mockReact.createElement('View', props);
19-
};
20-
return mockComponent;
21-
}),
7+
TurboModuleRegistry: {
8+
getEnforcing: jest.fn((name: string) => {
9+
throw new Error(
10+
`TurboModuleRegistry.getEnforcing(...): '${name}' could not be found.`,
11+
);
12+
}),
13+
},
2214
}));
2315

2416
describe('Native Module Linking', () => {
2517
it('throws error when native module is not linked', () => {
2618
expect(() => {
27-
// This will trigger the linking check
2819
require('../src/index');
29-
}).toThrow('@shopify/checkout-sheet-kit" is not correctly linked.');
20+
}).toThrow('ShopifyCheckoutSheetKit');
3021
});
3122
});

0 commit comments

Comments
 (0)