diff --git a/BARE_REACT_NATIVE_GUIDE.md b/BARE_REACT_NATIVE_GUIDE.md index 2da2685..3a0a280 100644 --- a/BARE_REACT_NATIVE_GUIDE.md +++ b/BARE_REACT_NATIVE_GUIDE.md @@ -8,9 +8,10 @@ This guide provides step-by-step instructions for integrating the Circle User-Co ## System Requirements - React Native 0.60+ (recommended 0.76-0.81) -- Node.js 16+ and npm/yarn -- Android API 21+ (recommended API 33+) +- Node.js 20.19+ and npm/yarn +- Android API 21+ (recommended API 36+) - iOS 15.1+ (recommended iOS 17+) +- Xcode 16.1+ (recommended 16.3+) - CocoaPods (for iOS projects) > [!NOTE] @@ -314,7 +315,7 @@ Learn more about [Running On Device](https://reactnative.dev/docs/running-on-dev **Issue**: `npx install-expo-modules@latest` fails with other errors -- **Solution**: Ensure Node.js 16+ is installed. Try clearing npm cache: `npm cache clean --force` +- **Solution**: Ensure Node.js 20.19+ is installed. Try clearing npm cache: `npm cache clean --force` ### Android Build Issues diff --git a/README.md b/README.md index 1420278..06aa36c 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,10 @@ You can also find the SDK v1 sample app in the sample app repository's [sdk-v1 b | ------------ | --------------- | ------------------- | | React Native | 0.60+ | 0.76-0.81 | | iOS | 15.1+ | iOS 17+ | -| Android | API 21+ | API 33+ | -| Expo SDK | 49+ | 53+ | +| Android | API 21+ | API 36+ | +| Expo SDK | 49+ | 54+ | +| Node.js | 20.19+ | 20.20+ | +| Xcode | 16.1+ | 16.3+ | ## Installation diff --git a/android/build.gradle b/android/build.gradle index a97dd3b..613a87a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -31,10 +31,10 @@ if (useManagedAndroidSdkVersions) { } } project.android { - compileSdkVersion safeExtGet("compileSdkVersion", 34) + compileSdkVersion safeExtGet("compileSdkVersion", 36) defaultConfig { minSdkVersion safeExtGet("minSdkVersion", 21) - targetSdkVersion safeExtGet("targetSdkVersion", 34) + targetSdkVersion safeExtGet("targetSdkVersion", 36) } } } diff --git a/package.json b/package.json index 7b8acfe..36cec2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@circle-fin/w3s-pw-react-native-sdk", - "version": "2.1.0", + "version": "2.2.1", "packageManager": "yarn@1.22.22", "description": "React Native SDK for Circle Programmable Wallet", "main": "build/index.js", @@ -73,23 +73,23 @@ "homepage": "https://github.com/circlefin/w3s-react-native-sdk.git #readme", "devDependencies": { "@babel/core": "^7.28.5", - "@expo/config-plugins": "~10.1.2", - "@types/jest": "^30.0.0", - "@types/react": "~19.0.0", + "@expo/config-plugins": "~54.0.4", + "@types/jest": "29.5.14", + "@types/react": "~19.1.10", "@typescript-eslint/eslint-plugin": "^8.38.0", "@typescript-eslint/parser": "^8.38.0", - "eslint": "^9.26.0", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-native": "^5.0.0", - "expo": "~53.0.12", - "expo-module-scripts": "^4.1.6", - "expo-modules-core": "~2.4.2", + "expo": "~54.0.0", + "expo-module-scripts": "^5.0.8", + "expo-modules-core": "~3.0.29", "jest": "^29.7.0", "prettier": "^3.6.2", - "react": "^19.2.0", - "react-native": "0.79.3", - "react-test-renderer": "^19.2.0", + "react": "~19.1.0", + "react-native": "0.81.5", + "react-test-renderer": "~19.1.0", "ts-jest": "^29.4.5", "typescript": "^5.9.3" }, @@ -103,5 +103,8 @@ "expo": { "optional": true } + }, + "resolutions": { + "@xmldom/xmldom": "0.8.12" } } diff --git a/src/__test__/WalletSdk.test.ts b/src/__test__/WalletSdk.test.ts index 30a3370..98daeef 100644 --- a/src/__test__/WalletSdk.test.ts +++ b/src/__test__/WalletSdk.test.ts @@ -42,7 +42,7 @@ const removeMocks: jest.Mock[] = [] */ function triggerEvent(eventName: string, payload: unknown): void { const callbacks = listenerMap.get(eventName) ?? [] - callbacks.forEach((cb) => cb(payload)) + callbacks.forEach(cb => cb(payload)) } const mockExecute = jest.fn() @@ -96,7 +96,11 @@ jest.mock('../ProgrammablewalletRnSdkModule', () => mockNativeModule) // Import WalletSdk AFTER mocks are set up import { WalletSdk } from '../WalletSdk' +import { ImageKey } from '../types' import type { LoginResult, SuccessResult } from '../types' +import { Image } from 'react-native' + +const mockResolveAssetSource = Image.resolveAssetSource as jest.Mock const SUCCESS_EVENT = 'CirclePwOnSuccess' const ERROR_EVENT = 'CirclePwOnError' @@ -138,7 +142,7 @@ describe('WalletSdk.execute', () => { it('invokes successCallback exactly once when event fires before Promise resolves', async () => { let resolvePromise!: (value: SuccessResult) => void mockExecute.mockReturnValue( - new Promise((res) => { + new Promise(res => { resolvePromise = res }), ) @@ -146,7 +150,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Event fires first triggerEvent(SUCCESS_EVENT, mockSuccessResult) @@ -165,7 +175,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Flush microtask queue so Promise .then runs await Promise.resolve() @@ -188,7 +204,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Error event fires first triggerEvent(ERROR_EVENT, { message: 'native error' }) @@ -207,7 +229,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Flush microtask queue so Promise .catch runs await Promise.resolve() @@ -225,7 +253,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Success event fires first triggerEvent(SUCCESS_EVENT, mockSuccessResult) @@ -243,7 +277,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Error event fires first triggerEvent(ERROR_EVENT, { message: 'native error' }) @@ -258,7 +298,7 @@ describe('WalletSdk.execute', () => { it('removes both listeners after success event', () => { let resolvePromise!: (value: SuccessResult) => void mockExecute.mockReturnValue( - new Promise((res) => { + new Promise(res => { resolvePromise = res }), ) @@ -266,7 +306,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) // Snapshot remove mocks registered so far (2: success + error listeners) const [successRemove, errorRemove] = removeMocks.slice(-2) @@ -291,7 +337,13 @@ describe('WalletSdk.execute', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.execute('token', 'key', ['challenge-1'], successCallback, errorCallback) + WalletSdk.execute( + 'token', + 'key', + ['challenge-1'], + successCallback, + errorCallback, + ) const [successRemove, errorRemove] = removeMocks.slice(-2) @@ -313,7 +365,7 @@ describe('WalletSdk.setBiometricsPin', () => { it('invokes successCallback exactly once when event fires before Promise resolves', async () => { let resolvePromise!: (value: SuccessResult) => void mockSetBiometricsPin.mockReturnValue( - new Promise((res) => { + new Promise(res => { resolvePromise = res }), ) @@ -425,7 +477,7 @@ describe('WalletSdk.setBiometricsPin', () => { it('removes both listeners after success event', () => { let resolvePromise!: (value: SuccessResult) => void mockSetBiometricsPin.mockReturnValue( - new Promise((res) => { + new Promise(res => { resolvePromise = res }), ) @@ -473,7 +525,10 @@ describe('WalletSdk.setBiometricsPin', () => { // verifyOTP() // --------------------------------------------------------------------------- -const mockLoginResult: LoginResult = { userToken: 'token', encryptionKey: 'key' } +const mockLoginResult: LoginResult = { + userToken: 'token', + encryptionKey: 'key', +} describe('WalletSdk.verifyOTP', () => { it('invokes errorCallback exactly once when error event fires before Promise rejects', async () => { @@ -487,7 +542,13 @@ describe('WalletSdk.verifyOTP', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.verifyOTP('otp', 'deviceToken', 'encKey', successCallback, errorCallback) + WalletSdk.verifyOTP( + 'otp', + 'deviceToken', + 'encKey', + successCallback, + errorCallback, + ) // Error event fires first triggerEvent(ERROR_EVENT, { message: 'native error' }) @@ -508,7 +569,13 @@ describe('WalletSdk.verifyOTP', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.verifyOTP('otp', 'deviceToken', 'encKey', successCallback, errorCallback) + WalletSdk.verifyOTP( + 'otp', + 'deviceToken', + 'encKey', + successCallback, + errorCallback, + ) // Flush microtask queue so Promise .catch + .finally run await Promise.resolve() @@ -527,7 +594,13 @@ describe('WalletSdk.verifyOTP', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.verifyOTP('otp', 'deviceToken', 'encKey', successCallback, errorCallback) + WalletSdk.verifyOTP( + 'otp', + 'deviceToken', + 'encKey', + successCallback, + errorCallback, + ) await Promise.resolve() await Promise.resolve() @@ -548,7 +621,13 @@ describe('WalletSdk.verifyOTP', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.verifyOTP('otp', 'deviceToken', 'encKey', successCallback, errorCallback) + WalletSdk.verifyOTP( + 'otp', + 'deviceToken', + 'encKey', + successCallback, + errorCallback, + ) const [errorRemove] = removeMocks.slice(-1) @@ -566,7 +645,13 @@ describe('WalletSdk.verifyOTP', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.verifyOTP('otp', 'deviceToken', 'encKey', successCallback, errorCallback) + WalletSdk.verifyOTP( + 'otp', + 'deviceToken', + 'encKey', + successCallback, + errorCallback, + ) const [errorRemove] = removeMocks.slice(-1) @@ -582,7 +667,13 @@ describe('WalletSdk.verifyOTP', () => { const successCallback = jest.fn() const errorCallback = jest.fn() - WalletSdk.verifyOTP('otp', 'deviceToken', 'encKey', successCallback, errorCallback) + WalletSdk.verifyOTP( + 'otp', + 'deviceToken', + 'encKey', + successCallback, + errorCallback, + ) const [errorRemove] = removeMocks.slice(-1) @@ -592,3 +683,55 @@ describe('WalletSdk.verifyOTP', () => { expect(errorRemove).toHaveBeenCalledTimes(1) }) }) + +// --------------------------------------------------------------------------- +// setImageMap() +// --------------------------------------------------------------------------- + +describe('WalletSdk.setImageMap', () => { + it('passes all entries to native module when all URIs are valid', () => { + mockResolveAssetSource.mockReturnValue({ uri: 'mock://asset' }) + + const map = new Map([ + [ImageKey.naviBack, 1], + [ImageKey.naviClose, 2], + ]) + + WalletSdk.setImageMap(map) + + expect(mockNativeModule.setImageMap).toHaveBeenCalledWith({ + [ImageKey.naviBack]: 'mock://asset', + [ImageKey.naviClose]: 'mock://asset', + }) + }) + + it('filters out entries where resolveAssetSource returns empty URI', () => { + mockResolveAssetSource + .mockReturnValueOnce({ uri: 'mock://valid' }) + .mockReturnValueOnce({ uri: '' }) + + const map = new Map([ + [ImageKey.naviBack, 1], + [ImageKey.naviClose, 2], + ]) + + WalletSdk.setImageMap(map) + + expect(mockNativeModule.setImageMap).toHaveBeenCalledWith({ + [ImageKey.naviBack]: 'mock://valid', + }) + }) + + it('calls native module with empty object when all URIs resolve to null', () => { + mockResolveAssetSource.mockReturnValue(null) + + const map = new Map([ + [ImageKey.naviBack, 1], + [ImageKey.naviClose, 2], + ]) + + WalletSdk.setImageMap(map) + + expect(mockNativeModule.setImageMap).toHaveBeenCalledWith({}) + }) +})