diff --git a/README.md b/README.md index afcd2440..50a87158 100644 --- a/README.md +++ b/README.md @@ -65,20 +65,21 @@ experiences. ## Platform Requirements -- **React Native** - Minimum version `0.76` (v4+) / `0.70` (v3 and earlier) +- **React Native** - Minimum version `0.76` (`3.9.0+`) / `0.70` (`<=3.8.x`) - **iOS** - Minimum version iOS 13 - **Android** - Minimum Java 11 & Android SDK version `23` ## Version Compatibility -Starting with **v4.0.0**, `@shopify/checkout-sheet-kit` requires the React Native -**New Architecture** (TurboModules + Fabric). Apps on the old architecture must -stay on the `v3.x` line until they migrate. +The **v3.9.x** line keeps the v3 public async API while supporting both React +Native architectures. Starting with **v4.0.0**, `@shopify/checkout-sheet-kit` +requires the React Native **New Architecture** (TurboModules + Fabric). | Package version | React Native | Architecture | | --------------- | -------------- | ------------------ | | `4.x` | `>= 0.76` | New Architecture | -| `3.x` | `>= 0.70` | Old Architecture | +| `3.9.x` | `>= 0.76` | Old + New | +| `<=3.8.x` | `>= 0.70` | Old Architecture | See the [React Native upgrade guide](https://reactnative.dev/docs/the-new-architecture/use-the-new-architecture) for help enabling the New Architecture in your app. diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts index 3e9d71eb..6d22da6b 100644 --- a/__mocks__/react-native.ts +++ b/__mocks__/react-native.ts @@ -76,6 +76,12 @@ module.exports = { requireNativeComponent, codegenNativeComponent, TurboModuleRegistry: { + get: jest.fn((name: string) => { + if (name === 'ShopifyCheckoutSheetKit') { + return ShopifyCheckoutSheetKit; + } + return null; + }), getEnforcing: jest.fn((name: string) => { if (name === 'ShopifyCheckoutSheetKit') { return ShopifyCheckoutSheetKit; diff --git a/modules/@shopify/checkout-sheet-kit/RNShopifyCheckoutSheetKit.podspec b/modules/@shopify/checkout-sheet-kit/RNShopifyCheckoutSheetKit.podspec index 59fa16c5..5d7a7a00 100644 --- a/modules/@shopify/checkout-sheet-kit/RNShopifyCheckoutSheetKit.podspec +++ b/modules/@shopify/checkout-sheet-kit/RNShopifyCheckoutSheetKit.podspec @@ -2,6 +2,10 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' + +new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" + Pod::Spec.new do |s| s.name = "RNShopifyCheckoutSheetKit" s.version = package["version"] @@ -19,5 +23,23 @@ Pod::Spec.new do |s| s.dependency "ShopifyCheckoutSheetKit", "~> 3.8.0" s.dependency "ShopifyCheckoutSheetKit/AcceleratedCheckouts", "~> 3.8.0" - install_modules_dependencies(s) + if new_arch_enabled + if defined?(install_modules_dependencies) + install_modules_dependencies(s) + else + s.dependency "React-Codegen" + s.dependency "RCT-Folly", :modular_headers => true + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + end + + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + end end diff --git a/modules/@shopify/checkout-sheet-kit/android/build.gradle b/modules/@shopify/checkout-sheet-kit/android/build.gradle index dd55f324..a8650ec4 100644 --- a/modules/@shopify/checkout-sheet-kit/android/build.gradle +++ b/modules/@shopify/checkout-sheet-kit/android/build.gradle @@ -10,7 +10,20 @@ buildscript { } apply plugin: "com.android.library" -apply plugin: "com.facebook.react" + +def isNewArchitectureEnabled() { + def newArchEnabled = project.hasProperty("newArchEnabled") + ? project.property("newArchEnabled") + : rootProject.hasProperty("newArchEnabled") + ? rootProject.property("newArchEnabled") + : "false" + + return newArchEnabled.toString() == "true" +} + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties[name]).toInteger() @@ -37,6 +50,10 @@ android { if (supportsNamespace()) { namespace "com.shopify.reactnative.checkoutsheetkit" + buildFeatures { + buildConfig true + } + sourceSets { main { manifest.srcFile "src/main/AndroidManifestNew.xml" @@ -50,6 +67,7 @@ android { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() } buildTypes { @@ -93,4 +111,3 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.12.5") debugImplementation("com.shopify:checkout-sheet-kit:${SHOPIFY_CHECKOUT_SDK_VERSION}") } - diff --git a/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitModule.java b/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitModule.java index 03427892..57bbaecd 100644 --- a/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitModule.java +++ b/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitModule.java @@ -27,21 +27,25 @@ of this software and associated documentation files (the "Software"), to deal import android.content.Context; import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; -import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; -import com.shopify.checkoutsheetkit.NativeShopifyCheckoutSheetKitSpec; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; import com.shopify.checkoutsheetkit.*; import java.util.HashMap; import java.util.Map; import java.util.Objects; -public class ShopifyCheckoutSheetKitModule extends NativeShopifyCheckoutSheetKitSpec { +@ReactModule(name = ShopifyCheckoutSheetKitModule.NAME) +public class ShopifyCheckoutSheetKitModule extends ReactContextBaseJavaModule implements TurboModule { + + public static final String NAME = "ShopifyCheckoutSheetKit"; public static Configuration checkoutConfig = new Configuration(); @@ -62,8 +66,14 @@ public ShopifyCheckoutSheetKitModule(ReactApplicationContext reactContext) { }); } + @NonNull + @Override + public String getName() { + return NAME; + } + @Override - protected Map getTypedExportedConstants() { + public Map getConstants() { final Map constants = new HashMap<>(); constants.put("version", ShopifyCheckoutSheetKit.version); return constants; diff --git a/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitPackage.java b/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitPackage.java index f6cda161..c97991dc 100644 --- a/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitPackage.java +++ b/modules/@shopify/checkout-sheet-kit/android/src/main/java/com/shopify/reactnative/checkoutsheetkit/ShopifyCheckoutSheetKitPackage.java @@ -26,7 +26,7 @@ of this software and associated documentation files (the "Software"), to deal import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.facebook.react.TurboReactPackage; +import com.facebook.react.BaseReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.module.model.ReactModuleInfo; @@ -38,7 +38,7 @@ of this software and associated documentation files (the "Software"), to deal import java.util.List; import java.util.Map; -public class ShopifyCheckoutSheetKitPackage extends TurboReactPackage { +public class ShopifyCheckoutSheetKitPackage extends BaseReactPackage { @NonNull @Override @@ -67,7 +67,7 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { false, // canOverrideExistingModule false, // needsEagerInit false, // isCxxModule - true // isTurboModule + BuildConfig.IS_NEW_ARCHITECTURE_ENABLED // isTurboModule )); return moduleInfos; }; diff --git a/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.mm b/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.mm index d93be8df..49a4ed23 100644 --- a/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.mm +++ b/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.mm @@ -24,6 +24,8 @@ of this software and associated documentation files (the "Software"), to deal #import #import + +#if RCT_NEW_ARCH_ENABLED #import // Registers the Swift module class (ShopifyCheckoutSheetKit.swift) with the RN @@ -61,6 +63,61 @@ @implementation RCTShopifyCheckoutSheetKit (TurboModule) params); } @end +#else +@interface RCT_EXTERN_MODULE (RCTShopifyCheckoutSheetKit, NSObject) + +/** + * Present checkout + */ +RCT_EXTERN_METHOD(present : (NSString*)checkoutURLString); + +/** + * Preload checkout + */ +RCT_EXTERN_METHOD(preload : (NSString*)checkoutURLString); + +/** + * Dismiss checkout + */ +RCT_EXTERN_METHOD(dismiss); + +/** + * Invalidate preload cache + */ +RCT_EXTERN_METHOD(invalidateCache); + +/** + * Set configuration for checkout + */ +RCT_EXTERN_METHOD(setConfig : (NSDictionary*)configuration); + +/** + * Return configuration for checkout + */ +RCT_EXTERN_METHOD(getConfig : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject); + +/** + * Configure AcceleratedCheckouts + */ +RCT_EXTERN_METHOD(configureAcceleratedCheckouts : (NSString*)storefrontDomain storefrontAccessToken : ( + NSString*)storefrontAccessToken customerEmail : (NSString*)customerEmail customerPhoneNumber : (NSString*) + customerPhoneNumber customerAccessToken : (NSString*)customerAccessToken applePayMerchantIdentifier : (NSString*) + applePayMerchantIdentifier applyPayContactFields : (NSArray*)applyPayContactFields supportedShippingCountries : (NSArray*)supportedShippingCountries resolve : ( + RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject); + +/** + * Check if accelerated checkout is available + */ +RCT_EXTERN_METHOD( + isAcceleratedCheckoutAvailable : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject); + +/** + * Check if Apple Pay is available + */ +RCT_EXTERN_METHOD(isApplePayAvailable : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject); + +@end +#endif /** * AcceleratedCheckoutButtons View Manager diff --git a/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.swift b/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.swift index e6b57522..1603d918 100644 --- a/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.swift +++ b/modules/@shopify/checkout-sheet-kit/ios/ShopifyCheckoutSheetKit.swift @@ -225,6 +225,13 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate { ] } + @objc func getConfig( + _ resolve: @escaping RCTPromiseResolveBlock, + reject _: @escaping RCTPromiseRejectBlock + ) { + resolve(getConfig()) + } + @objc func configureAcceleratedCheckouts( _ storefrontDomain: String, storefrontAccessToken: String, @@ -274,6 +281,30 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate { return NSNumber(value: true) } + @objc func configureAcceleratedCheckouts( + _ storefrontDomain: String, + storefrontAccessToken: String, + customerEmail: String?, + customerPhoneNumber: String?, + customerAccessToken: String?, + applePayMerchantIdentifier: String?, + applyPayContactFields: [String]?, + supportedShippingCountries: [String]?, + resolve: @escaping RCTPromiseResolveBlock, + reject _: @escaping RCTPromiseRejectBlock + ) { + resolve(configureAcceleratedCheckouts( + storefrontDomain, + storefrontAccessToken: storefrontAccessToken, + customerEmail: customerEmail, + customerPhoneNumber: customerPhoneNumber, + customerAccessToken: customerAccessToken, + applePayMerchantIdentifier: applePayMerchantIdentifier, + applyPayContactFields: applyPayContactFields, + supportedShippingCountries: supportedShippingCountries + )) + } + @objc func isAcceleratedCheckoutAvailable() -> NSNumber { guard #available(iOS 16.0, *) else { return NSNumber(value: false) @@ -282,6 +313,13 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate { return NSNumber(value: AcceleratedCheckoutConfiguration.shared.available) } + @objc func isAcceleratedCheckoutAvailable( + _ resolve: @escaping RCTPromiseResolveBlock, + reject _: @escaping RCTPromiseRejectBlock + ) { + resolve(isAcceleratedCheckoutAvailable()) + } + @objc func isApplePayAvailable() -> NSNumber { guard #available(iOS 16.0, *) else { return NSNumber(value: false) @@ -292,6 +330,13 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate { return NSNumber(value: available) } + @objc func isApplePayAvailable( + _ resolve: @escaping RCTPromiseResolveBlock, + reject _: @escaping RCTPromiseRejectBlock + ) { + resolve(isApplePayAvailable()) + } + @objc func initiateGeolocationRequest(_ allow: Bool) { // No-op on iOS — geolocation permission is handled natively } diff --git a/modules/@shopify/checkout-sheet-kit/package.json b/modules/@shopify/checkout-sheet-kit/package.json index c1347ba8..3280f64f 100644 --- a/modules/@shopify/checkout-sheet-kit/package.json +++ b/modules/@shopify/checkout-sheet-kit/package.json @@ -1,7 +1,7 @@ { "name": "@shopify/checkout-sheet-kit", "license": "MIT", - "version": "4.0.0", + "version": "3.9.0", "main": "lib/commonjs/index.js", "types": "src/index.ts", "source": "src/index.ts", diff --git a/modules/@shopify/checkout-sheet-kit/src/context.tsx b/modules/@shopify/checkout-sheet-kit/src/context.tsx index c3a5db4d..713c60c1 100644 --- a/modules/@shopify/checkout-sheet-kit/src/context.tsx +++ b/modules/@shopify/checkout-sheet-kit/src/context.tsx @@ -38,8 +38,8 @@ type Maybe = T | undefined; interface Context { acceleratedCheckoutsAvailable: boolean; addEventListener: AddEventListener; - getConfig: () => Configuration | undefined; - setConfig: (config: Configuration) => void; + getConfig: () => Promise; + setConfig: (config: Configuration) => Promise; removeEventListeners: RemoveEventListeners; preload: (checkoutUrl: string) => void; present: (checkoutUrl: string) => void; @@ -71,24 +71,28 @@ export function ShopifyCheckoutSheetProvider({ } useEffect(() => { - if (!instance.current || !configuration) { - return; - } - - const customer = configuration.acceleratedCheckouts?.customer; - if (customer?.accessToken && (customer?.email || customer?.phoneNumber)) { - // eslint-disable-next-line no-console - console.warn( - '[ShopifyCheckoutSheetKit] Providing accessToken with contactFields (email / phoneNumber) is deprecated and will become an error in v4.' + - 'When the user is authenticated with Customer Accounts, provide accessToken' + - 'When the user is otherwise authenticated, provide email/phoneNumber.', + async function configureCheckoutKit() { + if (!instance.current || !configuration) { + return; + } + + const customer = configuration.acceleratedCheckouts?.customer; + if (customer?.accessToken && (customer?.email || customer?.phoneNumber)) { + // eslint-disable-next-line no-console + console.warn( + '[ShopifyCheckoutSheetKit] Providing accessToken with contactFields (email / phoneNumber) is deprecated and will become an error in v4.' + + 'When the user is authenticated with Customer Accounts, provide accessToken' + + 'When the user is otherwise authenticated, provide email/phoneNumber.', + ); + } + + await instance.current.setConfig(configuration); + setAcceleratedCheckoutsAvailable( + instance.current.acceleratedCheckoutsReady, ); } - instance.current.setConfig(configuration); - setAcceleratedCheckoutsAvailable( - instance.current.acceleratedCheckoutsReady, - ); + configureCheckoutKit(); }, [configuration]); const addEventListener: AddEventListener = useCallback( @@ -122,11 +126,11 @@ export function ShopifyCheckoutSheetProvider({ instance.current?.dismiss(); }, []); - const setConfig = useCallback((config: Configuration) => { - instance.current?.setConfig(config); + const setConfig = useCallback(async (config: Configuration) => { + await instance.current?.setConfig(config); }, []); - const getConfig = useCallback(() => { + const getConfig = useCallback(async () => { return instance.current?.getConfig(); }, []); diff --git a/modules/@shopify/checkout-sheet-kit/src/index.d.ts b/modules/@shopify/checkout-sheet-kit/src/index.d.ts index 5a5d73b7..34147dad 100644 --- a/modules/@shopify/checkout-sheet-kit/src/index.d.ts +++ b/modules/@shopify/checkout-sheet-kit/src/index.d.ts @@ -325,7 +325,7 @@ export interface ShopifyCheckoutSheetKit { /** * Return the current config for the checkout. See README.md for more details. */ - getConfig(): Configuration; + getConfig(): Promise; /** * Listen for checkout events */ @@ -344,10 +344,10 @@ export interface ShopifyCheckoutSheetKit { */ configureAcceleratedCheckouts( config: AcceleratedCheckoutConfiguration, - ): boolean; + ): Promise; /** * Check if accelerated checkout is available for the given cart or product */ - isAcceleratedCheckoutAvailable(): boolean; + isAcceleratedCheckoutAvailable(): Promise; } diff --git a/modules/@shopify/checkout-sheet-kit/src/index.ts b/modules/@shopify/checkout-sheet-kit/src/index.ts index 8becbe0c..13fe173d 100644 --- a/modules/@shopify/checkout-sheet-kit/src/index.ts +++ b/modules/@shopify/checkout-sheet-kit/src/index.ts @@ -151,21 +151,23 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit { /** * Retrieves the current checkout configuration - * @returns The current Configuration + * @returns Promise containing the current Configuration */ - public getConfig(): Configuration { - return this.coerceConfigurationResult(RNShopifyCheckoutSheetKit.getConfig()); + public async getConfig(): Promise { + const config = await RNShopifyCheckoutSheetKit.getConfig(); + return this.coerceConfigurationResult(config); } /** * Updates the checkout configuration * @param configuration New configuration settings to apply */ - public setConfig(configuration: Configuration): void { + public async setConfig(configuration: Configuration): Promise { if (configuration.acceleratedCheckouts) { - this._acceleratedCheckoutsReady = this.configureAcceleratedCheckouts( - configuration.acceleratedCheckouts, - ); + this._acceleratedCheckoutsReady = + await this.configureAcceleratedCheckouts( + configuration.acceleratedCheckouts, + ); } RNShopifyCheckoutSheetKit.setConfig(configuration); } @@ -233,9 +235,9 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit { * Configure AcceleratedCheckouts for Shop Pay and Apple Pay buttons * @param config Configuration for AcceleratedCheckouts */ - public configureAcceleratedCheckouts( + public async configureAcceleratedCheckouts( config: AcceleratedCheckoutConfiguration, - ): boolean { + ): Promise { if (!this.acceleratedCheckoutsSupported) { return false; } @@ -265,9 +267,9 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit { /** * Check if accelerated checkout is available for the given cart or product - * @returns boolean indicating availability + * @returns Promise indicating availability */ - public isAcceleratedCheckoutAvailable(): boolean { + public async isAcceleratedCheckoutAvailable(): Promise { if (!this.acceleratedCheckoutsSupported) { return false; } diff --git a/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts b/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts index b2dd0fba..bc97e82f 100644 --- a/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts +++ b/modules/@shopify/checkout-sheet-kit/src/specs/NativeShopifyCheckoutSheetKit.ts @@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO */ import type {TurboModule} from 'react-native'; -import {TurboModuleRegistry} from 'react-native'; +import {NativeModules, TurboModuleRegistry} from 'react-native'; type IosColorsSpec = { tintColor?: string; @@ -96,6 +96,40 @@ export interface Spec extends TurboModule { getConstants(): {version: string}; } -export default TurboModuleRegistry.getEnforcing( - 'ShopifyCheckoutSheetKit', -); +type LegacyNativeModule = Omit & { + getConstants?: () => {version: string}; + version?: string; +}; + +const LINKING_ERROR = + "The native module 'ShopifyCheckoutSheetKit' from '@shopify/checkout-sheet-kit' doesn't seem to be linked. Make sure the native module is installed and rebuilt."; + +function withLegacyConstants(nativeModule: LegacyNativeModule): Spec { + if (typeof nativeModule.getConstants === 'function') { + return nativeModule as Spec; + } + + return Object.assign(Object.create(nativeModule), { + getConstants: () => ({version: nativeModule.version ?? ''}), + }) as Spec; +} + +function getNativeModule(): Spec { + const turboModule = TurboModuleRegistry.get('ShopifyCheckoutSheetKit'); + + if (turboModule != null) { + return turboModule; + } + + const nativeModule = NativeModules.ShopifyCheckoutSheetKit as + | LegacyNativeModule + | undefined; + + if (nativeModule == null) { + throw new Error(LINKING_ERROR); + } + + return withLegacyConstants(nativeModule); +} + +export default getNativeModule(); diff --git a/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx b/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx index 8f22f503..9e18f686 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx +++ b/modules/@shopify/checkout-sheet-kit/tests/context.test.tsx @@ -348,7 +348,7 @@ describe('useShopifyCheckoutSheet', () => { , ); - const config = hookValue.getConfig(); + const config = await hookValue.getConfig(); expect(config).toEqual({ preloading: true, colorScheme: 'automatic', diff --git a/modules/@shopify/checkout-sheet-kit/tests/index.test.ts b/modules/@shopify/checkout-sheet-kit/tests/index.test.ts index 8beeb28a..dc87f70d 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/index.test.ts +++ b/modules/@shopify/checkout-sheet-kit/tests/index.test.ts @@ -160,9 +160,9 @@ describe('ShopifyCheckoutSheetKit', () => { }); describe('getConfig', () => { - it('returns the parsed config from the Native Module', () => { + it('returns the parsed config from the Native Module', async () => { const instance = new ShopifyCheckoutSheet(); - expect(instance.getConfig()).toStrictEqual({ + await expect(instance.getConfig()).resolves.toStrictEqual({ preloading: true, colorScheme: ColorScheme.automatic, logLevel: LogLevel.error, @@ -732,7 +732,7 @@ describe('ShopifyCheckoutSheetKit', () => { NativeModule.configureAcceleratedCheckouts.mockReturnValue(true); const result = - instance.configureAcceleratedCheckouts(acceleratedConfig); + await instance.configureAcceleratedCheckouts(acceleratedConfig); expect(result).toBe(true); expect( @@ -757,7 +757,7 @@ describe('ShopifyCheckoutSheetKit', () => { }; NativeModule.configureAcceleratedCheckouts.mockReturnValue(true); - instance.configureAcceleratedCheckouts(minimalConfig); + await instance.configureAcceleratedCheckouts(minimalConfig); expect( NativeModule.configureAcceleratedCheckouts, @@ -778,7 +778,7 @@ describe('ShopifyCheckoutSheetKit', () => { const instance = new ShopifyCheckoutSheet(); const result = - instance.configureAcceleratedCheckouts(acceleratedConfig); + await instance.configureAcceleratedCheckouts(acceleratedConfig); expect(result).toBe(false); expect( @@ -794,9 +794,9 @@ describe('ShopifyCheckoutSheetKit', () => { }; const expectedError = new Error('`storefrontDomain` is required'); - expect( + await expect( instance.configureAcceleratedCheckouts(invalidConfig), - ).toBe(false); + ).resolves.toBe(false); expect(console.error).toHaveBeenCalledWith( '[ShopifyCheckoutSheetKit] Failed to configure accelerated checkouts with', expectedError, @@ -812,9 +812,9 @@ describe('ShopifyCheckoutSheetKit', () => { const expectedError = new Error('`storefrontAccessToken` is required'); - expect( + await expect( instance.configureAcceleratedCheckouts(invalidConfig), - ).toBe(false); + ).resolves.toBe(false); expect(console.error).toHaveBeenCalledWith( '[ShopifyCheckoutSheetKit] Failed to configure accelerated checkouts with', expectedError, @@ -837,9 +837,9 @@ describe('ShopifyCheckoutSheetKit', () => { '`wallets.applePay.merchantIdentifier` is required', ); - expect( + await expect( instance.configureAcceleratedCheckouts(invalidConfig), - ).toBe(false); + ).resolves.toBe(false); expect(console.error).toHaveBeenCalledWith( '[ShopifyCheckoutSheetKit] Failed to configure accelerated checkouts with', expectedError, @@ -862,16 +862,16 @@ describe('ShopifyCheckoutSheetKit', () => { `'wallets.applePay.contactFields' contains unexpected values. Expected "email, phone", received "invalid"`, ); - expect( + await expect( instance.configureAcceleratedCheckouts(invalidConfig as any), - ).toBe(false); + ).resolves.toBe(false); expect(console.error).toHaveBeenCalledWith( '[ShopifyCheckoutSheetKit] Failed to configure accelerated checkouts with', expectedError, ); }); - it('does not throw when Apple Pay wallet is not configured', () => { + it('does not throw when Apple Pay wallet is not configured', async () => { const instance = new ShopifyCheckoutSheet(); const configWithoutApplePay = { storefrontDomain: 'test-shop.myshopify.com', @@ -879,12 +879,12 @@ describe('ShopifyCheckoutSheetKit', () => { }; NativeModule.configureAcceleratedCheckouts.mockReturnValue(true); - expect( + await expect( instance.configureAcceleratedCheckouts(configWithoutApplePay), - ).toBe(true); + ).resolves.toBe(true); }); - it('throws when a non-string value is given for supportedShippingCountries', () => { + it('throws when a non-string value is given for supportedShippingCountries', async () => { const instance = new ShopifyCheckoutSheet(); const invalidConfig = { ...acceleratedConfig, @@ -901,9 +901,9 @@ describe('ShopifyCheckoutSheetKit', () => { `'wallets.applePay.supportedShippingCountries' contains unexpected values. Expects ISO 3166-1 alpha-2 country codes (e.g., "US", "CA", "GB").`, ); - expect( + await expect( instance.configureAcceleratedCheckouts(invalidConfig as any), - ).toBe(false); + ).resolves.toBe(false); expect(console.error).toHaveBeenCalledWith( '[ShopifyCheckoutSheetKit] Failed to configure accelerated checkouts with', expectedError, @@ -913,7 +913,7 @@ describe('ShopifyCheckoutSheetKit', () => { it('calls configureAcceleratedCheckouts with an empty array for supportShippingCountries when omitted', async () => { const instance = new ShopifyCheckoutSheet(); - instance.configureAcceleratedCheckouts({ + await instance.configureAcceleratedCheckouts({ ...acceleratedConfig, wallets: { applePay: { @@ -940,7 +940,7 @@ describe('ShopifyCheckoutSheetKit', () => { it('calls configureAcceleratedCheckouts with supportShippingCountries when given', async () => { const instance = new ShopifyCheckoutSheet(); - instance.configureAcceleratedCheckouts({ + await instance.configureAcceleratedCheckouts({ ...acceleratedConfig, wallets: { applePay: { @@ -967,25 +967,25 @@ describe('ShopifyCheckoutSheetKit', () => { }); describe('isAcceleratedCheckoutAvailable', () => { - it('calls native isAcceleratedCheckoutAvailable on iOS', () => { + it('calls native isAcceleratedCheckoutAvailable on iOS', async () => { const instance = new ShopifyCheckoutSheet(); NativeModule.isAcceleratedCheckoutAvailable.mockReturnValue(true); const result = instance.isAcceleratedCheckoutAvailable(); - expect(result).toBe(true); + await expect(result).resolves.toBe(true); expect( NativeModule.isAcceleratedCheckoutAvailable, ).toHaveBeenCalledTimes(1); }); - it('returns false on Android', () => { + it('returns false on Android', async () => { Platform.OS = 'android'; const instance = new ShopifyCheckoutSheet(); const result = instance.isAcceleratedCheckoutAvailable(); - expect(result).toBe(false); + await expect(result).resolves.toBe(false); expect( NativeModule.isAcceleratedCheckoutAvailable, ).not.toHaveBeenCalled(); diff --git a/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts b/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts index f83795f8..91e3fff2 100644 --- a/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts +++ b/modules/@shopify/checkout-sheet-kit/tests/linking.test.ts @@ -1,22 +1,114 @@ -jest.mock('react-native', () => ({ - NativeModules: {}, - NativeEventEmitter: jest.fn(), - Platform: { - OS: 'ios', - }, - TurboModuleRegistry: { - getEnforcing: jest.fn((name: string) => { - throw new Error( - `TurboModuleRegistry.getEnforcing(...): '${name}' could not be found.`, - ); - }), - }, -})); +function createNativeModule(version: string) { + return { + version, + getConstants: jest.fn(() => ({version})), + preload: jest.fn(), + present: jest.fn(), + dismiss: jest.fn(), + invalidateCache: jest.fn(), + getConfig: jest.fn(() => ({ + preloading: true, + colorScheme: 'automatic', + logLevel: 'error', + })), + setConfig: jest.fn(), + addEventListener: jest.fn(), + removeEventListeners: jest.fn(), + initiateGeolocationRequest: jest.fn(), + configureAcceleratedCheckouts: jest.fn(() => true), + isAcceleratedCheckoutAvailable: jest.fn(() => true), + isApplePayAvailable: jest.fn(() => true), + addListener: jest.fn(), + removeListeners: jest.fn(), + }; +} + +function createLegacyNativeModule(version: string) { + const {getConstants: _getConstants, ...nativeModule} = + createNativeModule(version); + + return nativeModule; +} + +function mockReactNative({ + turboModule, + legacyModule, +}: { + turboModule?: ReturnType | null; + legacyModule?: + | ReturnType + | ReturnType + | null; +}) { + jest.doMock('react-native', () => ({ + NativeModules: legacyModule ? {ShopifyCheckoutSheetKit: legacyModule} : {}, + NativeEventEmitter: jest.fn(() => ({ + addListener: jest.fn(), + removeAllListeners: jest.fn(), + })), + PermissionsAndroid: { + requestMultiple: jest.fn(async () => ({})), + }, + Platform: { + OS: 'ios', + Version: '16.0', + }, + TurboModuleRegistry: { + get: jest.fn((name: string) => + name === 'ShopifyCheckoutSheetKit' ? turboModule : null, + ), + getEnforcing: jest.fn((name: string) => { + if (name === 'ShopifyCheckoutSheetKit' && turboModule) { + return turboModule; + } + throw new Error( + `TurboModuleRegistry.getEnforcing(...): '${name}' could not be found.`, + ); + }), + }, + codegenNativeComponent: jest.fn(() => 'RCTAcceleratedCheckoutButtons'), + requireNativeComponent: jest.fn(() => 'RCTAcceleratedCheckoutButtons'), + StyleSheet: { + flatten: jest.fn(style => style), + }, + })); +} describe('Native Module Linking', () => { + beforeEach(() => { + jest.resetModules(); + }); + + afterEach(() => { + jest.dontMock('react-native'); + }); + + it('uses the TurboModule when it is available', () => { + mockReactNative({turboModule: createNativeModule('turbo')}); + + const {ShopifyCheckoutSheet} = require('../src'); + const checkoutSheet = new ShopifyCheckoutSheet(); + + expect(checkoutSheet.version).toBe('turbo'); + }); + + it('falls back to the legacy NativeModules bridge', () => { + mockReactNative({ + turboModule: null, + legacyModule: createLegacyNativeModule('legacy'), + }); + + const {ShopifyCheckoutSheet} = require('../src'); + const checkoutSheet = new ShopifyCheckoutSheet(); + + expect(checkoutSheet.version).toBe('legacy'); + }); + it('throws error when native module is not linked', () => { + mockReactNative({turboModule: null, legacyModule: null}); + expect(() => { - require('../src/index'); + require('../src'); }).toThrow('ShopifyCheckoutSheetKit'); }); }); diff --git a/sample/ios/Podfile.lock b/sample/ios/Podfile.lock index 07faf199..dd6728c1 100644 --- a/sample/ios/Podfile.lock +++ b/sample/ios/Podfile.lock @@ -2578,7 +2578,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNShopifyCheckoutSheetKit (4.0.0): + - RNShopifyCheckoutSheetKit (3.9.0): - boost - DoubleConversion - fast_float @@ -2996,7 +2996,7 @@ SPEC CHECKSUMS: RNGestureHandler: eeb622199ef1fb3a076243131095df1c797072f0 RNReanimated: 237d420b7bb4378ef1dacc7d7a5c674fddb4b5d2 RNScreens: 3fc29af06302e1f1c18a7829fe57cbc2c0259912 - RNShopifyCheckoutSheetKit: 2a8c97d7780466538843d4cb1368c7ed76a33689 + RNShopifyCheckoutSheetKit: 04a94fbd56700f61478c658307eec38fb6c9439b RNVectorIcons: be4d047a76ad307ffe54732208fb0498fcb8477f ShopifyCheckoutSheetKit: 5253ca4da4c4f31069286509693930d02b4150d8 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 diff --git a/sample/src/context/Cart.tsx b/sample/src/context/Cart.tsx index a3b15272..c4235be5 100644 --- a/sample/src/context/Cart.tsx +++ b/sample/src/context/Cart.tsx @@ -119,9 +119,9 @@ export const CartProvider: React.FC = ({children}) => { }, [cartId, fetchCart, setTotalQuantity]); const preloadCheckout = useCallback( - (checkoutURL: string) => { + async (checkoutURL: string) => { if (checkoutURL) { - const config = shopify.getConfig(); + const config = await shopify.getConfig(); if (config?.preloading) { shopify.preload(checkoutURL); } diff --git a/sample/src/screens/SettingsScreen.tsx b/sample/src/screens/SettingsScreen.tsx index 2d670204..5de4ce4c 100644 --- a/sample/src/screens/SettingsScreen.tsx +++ b/sample/src/screens/SettingsScreen.tsx @@ -101,8 +101,11 @@ function SettingsScreen() { const [preloadingEnabled, setPreloadingEnabled] = useState(false); useEffect(() => { - const config = shopify.getConfig(); - setPreloadingEnabled(config?.preloading ?? false); + async function loadConfig() { + const config = await shopify.getConfig(); + setPreloadingEnabled(config?.preloading ?? false); + } + loadConfig(); }, [shopify]); const handleColorSchemeChange = useCallback( @@ -116,8 +119,8 @@ function SettingsScreen() { [appConfig, setAppConfig, setColorScheme], ); - const handleTogglePreloading = useCallback(() => { - const currentConfig = shopify.getConfig(); + const handleTogglePreloading = useCallback(async () => { + const currentConfig = await shopify.getConfig(); const newPreloadingValue = !currentConfig?.preloading; shopify.setConfig({ ...currentConfig,