diff --git a/__mocks__/codegenNativeComponent.ts b/__mocks__/codegenNativeComponent.ts new file mode 100644 index 00000000..56bc43de --- /dev/null +++ b/__mocks__/codegenNativeComponent.ts @@ -0,0 +1,11 @@ +const React = require('react'); + +const codegenNativeComponent = (_name: string) => { + return (props: any) => + React.createElement('View', { + ...props, + testID: props?.testID ?? 'accelerated-checkout-buttons', + }); +}; + +export default codegenNativeComponent; diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 34cb204a..6dd8d395 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -47,6 +47,7 @@ const exampleConfig = {preloading: true}; const ShopifyCheckoutSheetKit = { version: '0.7.0', + getConstants: jest.fn(() => ({version: '0.7.0'})), preload: jest.fn(), present: jest.fn(), dismiss: jest.fn(), @@ -58,6 +59,8 @@ const ShopifyCheckoutSheetKit = { initiateGeolocationRequest: jest.fn(), configureAcceleratedCheckouts: jest.fn(), isAcceleratedCheckoutAvailable: jest.fn(), + addListener: jest.fn(), + removeListeners: jest.fn(), }; // CommonJS export for Jest manual mock resolution @@ -68,6 +71,14 @@ module.exports = { }, NativeEventEmitter: jest.fn(() => createMockEmitter()), requireNativeComponent, + TurboModuleRegistry: { + getEnforcing: jest.fn((name: string) => { + if (name === 'ShopifyCheckoutSheetKit') { + return ShopifyCheckoutSheetKit; + } + return null; + }), + }, NativeModules: { ShopifyCheckoutSheetKit: { ...ShopifyCheckoutSheetKit, diff --git a/jest.config.js b/jest.config.js index 6763de59..d43f8497 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,10 @@ module.exports = { modulePathIgnorePatterns: ['modules/@shopify/checkout-sheet-kit/lib'], modulePaths: ['/sample/node_modules'], setupFiles: ['/jest.setup.ts'], + moduleNameMapper: { + 'react-native/Libraries/Utilities/codegenNativeComponent': + '/__mocks__/codegenNativeComponent.ts', + }, transform: { '\\.[jt]sx?$': 'babel-jest', }, diff --git a/modules/@shopify/checkout-sheet-kit/package.json b/modules/@shopify/checkout-sheet-kit/package.json index 64c3421c..79c1b3a0 100644 --- a/modules/@shopify/checkout-sheet-kit/package.json +++ b/modules/@shopify/checkout-sheet-kit/package.json @@ -54,6 +54,14 @@ "react-native-builder-bob": "^0.23.2", "typescript": "^5.9.2" }, + "codegenConfig": { + "name": "RNShopifyCheckoutSheetKitSpec", + "type": "all", + "jsSrcsDir": "src/specs", + "android": { + "javaPackageName": "com.shopify.checkoutsheetkit" + } + }, "react-native-builder-bob": { "source": "src", "output": "lib", diff --git a/modules/@shopify/checkout-sheet-kit/package.snapshot.json b/modules/@shopify/checkout-sheet-kit/package.snapshot.json index 782ccbaa..8cddcc9f 100644 --- a/modules/@shopify/checkout-sheet-kit/package.snapshot.json +++ b/modules/@shopify/checkout-sheet-kit/package.snapshot.json @@ -30,6 +30,10 @@ "lib/commonjs/index.js.map", "lib/commonjs/pixels.d.js", "lib/commonjs/pixels.d.js.map", + "lib/commonjs/specs/NativeShopifyCheckoutSheetKit.js", + "lib/commonjs/specs/NativeShopifyCheckoutSheetKit.js.map", + "lib/commonjs/specs/RCTAcceleratedCheckoutButtonsNativeComponent.js", + "lib/commonjs/specs/RCTAcceleratedCheckoutButtonsNativeComponent.js.map", "lib/module/components/AcceleratedCheckoutButtons.js", "lib/module/components/AcceleratedCheckoutButtons.js.map", "lib/module/context.js", @@ -44,12 +48,20 @@ "lib/module/index.js.map", "lib/module/pixels.d.js", "lib/module/pixels.d.js.map", + "lib/module/specs/NativeShopifyCheckoutSheetKit.js", + "lib/module/specs/NativeShopifyCheckoutSheetKit.js.map", + "lib/module/specs/RCTAcceleratedCheckoutButtonsNativeComponent.js", + "lib/module/specs/RCTAcceleratedCheckoutButtonsNativeComponent.js.map", "lib/typescript/src/components/AcceleratedCheckoutButtons.d.ts", "lib/typescript/src/components/AcceleratedCheckoutButtons.d.ts.map", "lib/typescript/src/context.d.ts", "lib/typescript/src/context.d.ts.map", "lib/typescript/src/index.d.ts", "lib/typescript/src/index.d.ts.map", + "lib/typescript/src/specs/NativeShopifyCheckoutSheetKit.d.ts", + "lib/typescript/src/specs/NativeShopifyCheckoutSheetKit.d.ts.map", + "lib/typescript/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.d.ts", + "lib/typescript/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.d.ts.map", "package.json", "src/components/AcceleratedCheckoutButtons.tsx", "src/context.tsx", @@ -57,5 +69,7 @@ "src/events.d.ts", "src/index.d.ts", "src/index.ts", - "src/pixels.d.ts" + "src/pixels.d.ts", + "src/specs/NativeShopifyCheckoutSheetKit.ts", + "src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts" ] diff --git a/modules/@shopify/checkout-sheet-kit/src/components/AcceleratedCheckoutButtons.tsx b/modules/@shopify/checkout-sheet-kit/src/components/AcceleratedCheckoutButtons.tsx index 7958fe85..ae770bb0 100644 --- a/modules/@shopify/checkout-sheet-kit/src/components/AcceleratedCheckoutButtons.tsx +++ b/modules/@shopify/checkout-sheet-kit/src/components/AcceleratedCheckoutButtons.tsx @@ -22,8 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO */ import React, {useCallback, useMemo, useState} from 'react'; -import {requireNativeComponent, Platform} from 'react-native'; +import {Platform} from 'react-native'; import type {ViewStyle} from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; import type { AcceleratedCheckoutWallet, CheckoutCompletedEvent, @@ -164,7 +165,7 @@ interface NativeAcceleratedCheckoutButtonsProps { } const RCTAcceleratedCheckoutButtons = - requireNativeComponent( + codegenNativeComponent( 'RCTAcceleratedCheckoutButtons', ); diff --git a/modules/@shopify/checkout-sheet-kit/src/index.ts b/modules/@shopify/checkout-sheet-kit/src/index.ts index 964f6312..89ff238b 100644 --- a/modules/@shopify/checkout-sheet-kit/src/index.ts +++ b/modules/@shopify/checkout-sheet-kit/src/index.ts @@ -21,17 +21,13 @@ 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 { - NativeModules, - NativeEventEmitter, - PermissionsAndroid, - Platform, -} from 'react-native'; +import {NativeEventEmitter, PermissionsAndroid, Platform} from 'react-native'; import type { EmitterSubscription, EventSubscription, PermissionStatus, } from 'react-native'; +import NativeShopifyCheckoutSheetKit from './specs/NativeShopifyCheckoutSheetKit'; import {ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet} from './context'; import {ApplePayContactField, ColorScheme, LogLevel} from './index.d'; import type { @@ -64,14 +60,7 @@ import type { RenderStateChangeEvent, } from './components/AcceleratedCheckoutButtons'; -const RNShopifyCheckoutSheetKit = NativeModules.ShopifyCheckoutSheetKit; - -if (!('ShopifyCheckoutSheetKit' in NativeModules)) { - throw new Error(` - "@shopify/checkout-sheet-kit" is not correctly linked. - - If you are building for iOS, make sure to run "pod install" first and restart the metro server.`); -} +const RNShopifyCheckoutSheetKit = NativeShopifyCheckoutSheetKit; const defaultFeatures: Features = { handleGeolocationRequests: true, @@ -114,7 +103,8 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit { } } - public readonly version: string = RNShopifyCheckoutSheetKit.version; + public readonly version: string = + RNShopifyCheckoutSheetKit.getConstants().version; /** * Dismisses the currently displayed checkout sheet @@ -151,7 +141,7 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit { * @returns Promise containing the current Configuration */ public async getConfig(): Promise { - return RNShopifyCheckoutSheetKit.getConfig(); + return RNShopifyCheckoutSheetKit.getConfig() as Promise; } /** diff --git a/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts b/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts new file mode 100644 index 00000000..89855ea5 --- /dev/null +++ b/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts @@ -0,0 +1,101 @@ +/* +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 type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; + +type IosColorsSpec = { + tintColor?: string; + backgroundColor?: string; + closeButtonColor?: string; +}; + +type AndroidColorsBaseSpec = { + progressIndicator?: string; + backgroundColor?: string; + headerBackgroundColor?: string; + headerTextColor?: string; + closeButtonColor?: string; +}; + +type AndroidColorsSpec = { + progressIndicator?: string; + backgroundColor?: string; + headerBackgroundColor?: string; + headerTextColor?: string; + closeButtonColor?: string; + light?: AndroidColorsBaseSpec; + dark?: AndroidColorsBaseSpec; +}; + +type ColorsSpec = { + ios?: IosColorsSpec; + android?: AndroidColorsSpec; +}; + +type ConfigurationSpec = { + preloading?: boolean; + title?: string; + colorScheme?: string; + logLevel?: string; + colors?: ColorsSpec; +}; + +type ConfigurationResultSpec = { + preloading: boolean; + colorScheme: string; + logLevel: string; + title?: string; + tintColor?: string; + backgroundColor?: string; + closeButtonColor?: string; +}; + +export interface Spec extends TurboModule { + present(checkoutUrl: string): void; + preload(checkoutUrl: string): void; + dismiss(): void; + invalidateCache(): void; + setConfig(configuration: ConfigurationSpec): void; + getConfig(): Promise; + configureAcceleratedCheckouts( + storefrontDomain: string, + storefrontAccessToken: string, + customerEmail: string | null, + customerPhoneNumber: string | null, + customerAccessToken: string | null, + applePayMerchantIdentifier: string | null, + applyPayContactFields: string[], + supportedShippingCountries: string[], + ): Promise; + isAcceleratedCheckoutAvailable(): Promise; + isApplePayAvailable(): Promise; + initiateGeolocationRequest(allow: boolean): void; + addListener(eventName: string): void; + removeListeners(count: number): void; + getConstants(): {version: string}; +} + +export default TurboModuleRegistry.getEnforcing( + 'ShopifyCheckoutSheetKit', +); diff --git a/modules/@shopify/checkout-sheet-kit/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts b/modules/@shopify/checkout-sheet-kit/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts new file mode 100644 index 00000000..b5d62ff8 --- /dev/null +++ b/modules/@shopify/checkout-sheet-kit/src/specs/RCTAcceleratedCheckoutButtonsNativeComponent.ts @@ -0,0 +1,81 @@ +/* +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 type {ViewProps} from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { + BubblingEventHandler, + DirectEventHandler, + Double, + Float, +} from 'react-native/Libraries/Types/CodegenTypes'; + +type FailEvent = Readonly<{ + __typename: string; + message: string; + code?: string; + recoverable?: boolean; +}>; + +type CompleteEvent = Readonly<{ + orderDetails: Readonly<{ + id: string; + email?: string; + phone?: string; + }>; +}>; + +type RenderStateChangeEvent = Readonly<{ + state: string; + reason?: string; +}>; + +type ClickLinkEvent = Readonly<{url: string}>; +type SizeChangeEvent = Readonly<{height: Double}>; + +type CheckoutIdentifierSpec = Readonly<{ + cartId?: string; + variantId?: string; + quantity?: Double; +}>; + +interface NativeProps extends ViewProps { + checkoutIdentifier: CheckoutIdentifierSpec; + cornerRadius?: Float; + wallets?: ReadonlyArray; + applePayLabel?: string; + onFail?: BubblingEventHandler; + onComplete?: BubblingEventHandler; + onCancel?: BubblingEventHandler; + onRenderStateChange?: BubblingEventHandler; + onWebPixelEvent?: BubblingEventHandler>; + onClickLink?: BubblingEventHandler; + onSizeChange?: DirectEventHandler; + onShouldRecoverFromError?: DirectEventHandler< + Readonly<{recoverable: boolean}> + >; +} + +export default codegenNativeComponent( + 'RCTAcceleratedCheckoutButtons', +); diff --git a/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts b/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts index b2587f05..f83795f8 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts +++ b/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts @@ -1,31 +1,22 @@ -/** - * Test for native module linking error - */ - -// Mock NativeModules without ShopifyCheckoutSheetKit jest.mock('react-native', () => ({ - NativeModules: { - // Intentionally empty to trigger linking error - }, + NativeModules: {}, NativeEventEmitter: jest.fn(), Platform: { OS: 'ios', }, - requireNativeComponent: jest.fn().mockImplementation(() => { - const mockComponent = (props: any) => { - // Use React.createElement with plain object instead - const mockReact = jest.requireActual('react'); - return mockReact.createElement('View', props); - }; - return mockComponent; - }), + TurboModuleRegistry: { + getEnforcing: jest.fn((name: string) => { + throw new Error( + `TurboModuleRegistry.getEnforcing(...): '${name}' could not be found.`, + ); + }), + }, })); describe('Native Module Linking', () => { it('throws error when native module is not linked', () => { expect(() => { - // This will trigger the linking check require('../src/index'); - }).toThrow('@shopify/checkout-sheet-kit" is not correctly linked.'); + }).toThrow('ShopifyCheckoutSheetKit'); }); }); diff --git a/sample/android/settings.gradle b/sample/android/settings.gradle index f49ac411..53360013 100644 --- a/sample/android/settings.gradle +++ b/sample/android/settings.gradle @@ -1,4 +1,11 @@ -pluginManagement { includeBuild("../../node_modules/@react-native/gradle-plugin") } +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } + includeBuild("../../node_modules/@react-native/gradle-plugin") +} plugins { id("com.facebook.react.settings") } extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } diff --git a/sample/package.json b/sample/package.json index 3c70b08d..206396f1 100644 --- a/sample/package.json +++ b/sample/package.json @@ -10,8 +10,8 @@ "release:android": "sh ./scripts/release_android", "build:ios": "sh ./scripts/build_ios", "lint": "yarn typecheck && eslint .", - "ios": "react-native run-ios --simulator 'iPhone 15 Pro'", - "start": "react-native start -- --simulator 'iPhone 15 Pro' --reset-cache", + "ios": "react-native run-ios", + "start": "react-native start -- --reset-cache", "typecheck": "tsc --noEmit", "test:ios": "sh ./scripts/test_ios", "test:android": "sh ./scripts/test_android"