From a70f6d72ba973b9330971c6adff83860f56397c1 Mon Sep 17 00:00:00 2001 From: kilian-tennyson Date: Fri, 4 Jul 2025 11:29:18 +0000 Subject: [PATCH] feat(SDK): Implement setUserJWT API for iOS/Android, update Intercom iOS SDK to 18.2.0 --- __tests__/setUserJWT.test.js | 100 ++++++++++++++++++ .../reactnative/IntercomErrorCodes.java | 11 +- .../intercom/reactnative/IntercomModule.java | 12 +++ intercom-react-native.podspec | 2 +- ios/IntercomModule.m | 12 +++ src/index.tsx | 14 +++ 6 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 __tests__/setUserJWT.test.js diff --git a/__tests__/setUserJWT.test.js b/__tests__/setUserJWT.test.js new file mode 100644 index 00000000..0551957a --- /dev/null +++ b/__tests__/setUserJWT.test.js @@ -0,0 +1,100 @@ +/** + * Tests for setUserJWT functionality + */ + +const { NativeModules } = require('react-native'); +const Intercom = require('../src/index').default; + +// Mock the native module +jest.mock('react-native', () => ({ + NativeModules: { + IntercomModule: { + setUserJwt: jest.fn(), + }, + IntercomEventEmitter: { + UNREAD_COUNT_CHANGE_NOTIFICATION: + 'IntercomUnreadCountDidChangeNotification', + WINDOW_DID_HIDE_NOTIFICATION: 'IntercomWindowDidHideNotification', + WINDOW_DID_SHOW_NOTIFICATION: 'IntercomWindowDidShowNotification', + }, + }, + NativeEventEmitter: jest.fn(), + Platform: { + OS: 'ios', + select: jest.fn((config) => config.ios || config.default), + }, +})); + +describe('setUserJWT', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should call native setUserJwt method with correct JWT token', async () => { + const mockJwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + + NativeModules.IntercomModule.setUserJwt.mockResolvedValue(true); + + const result = await Intercom.setUserJwt(mockJwt); + + expect(NativeModules.IntercomModule.setUserJwt).toHaveBeenCalledWith( + mockJwt + ); + expect(result).toBe(true); + }); + + test('should handle empty JWT token', async () => { + const emptyJwt = ''; + + NativeModules.IntercomModule.setUserJwt.mockResolvedValue(true); + + const result = await Intercom.setUserJwt(emptyJwt); + + expect(NativeModules.IntercomModule.setUserJwt).toHaveBeenCalledWith( + emptyJwt + ); + expect(result).toBe(true); + }); + + test('should handle null JWT token', async () => { + const nullJwt = null; + + NativeModules.IntercomModule.setUserJwt.mockResolvedValue(true); + + const result = await Intercom.setUserJwt(nullJwt); + + expect(NativeModules.IntercomModule.setUserJwt).toHaveBeenCalledWith( + nullJwt + ); + expect(result).toBe(true); + }); + + test('should handle rejection from native module', async () => { + const mockJwt = 'invalid-jwt-token'; + const mockError = new Error('Invalid JWT token format'); + + NativeModules.IntercomModule.setUserJwt.mockRejectedValue(mockError); + + await expect(Intercom.setUserJwt(mockJwt)).rejects.toThrow( + 'Invalid JWT token format' + ); + expect(NativeModules.IntercomModule.setUserJwt).toHaveBeenCalledWith( + mockJwt + ); + }); + + test('should handle complex JWT token structure', async () => { + const complexJwt = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyLCJhdWQiOiJpbnRlcmNvbS1hcHAiLCJpc3MiOiJteS1hcHAifQ.K1fOjxHcxtRYFfKZPEfUCzUXLp8sSpgPbMa8t0gE6A0'; + + NativeModules.IntercomModule.setUserJwt.mockResolvedValue(true); + + const result = await Intercom.setUserJwt(complexJwt); + + expect(NativeModules.IntercomModule.setUserJwt).toHaveBeenCalledWith( + complexJwt + ); + expect(result).toBe(true); + }); +}); diff --git a/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java b/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java index 68b7ba76..3d69208e 100644 --- a/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java +++ b/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java @@ -4,11 +4,12 @@ public class IntercomErrorCodes { public static final String UNIDENTIFIED_REGISTRATION = "101"; public static final String IDENTIFIED_REGISTRATION = "102"; public static final String SET_USER_HASH = "103"; - public static final String UPDATE_USER_HASH = "104"; - public static final String LOG_EVENT_HASH = "105"; - public static final String LOGOUT = "106"; - public static final String SET_LOG_LEVEL = "107"; - public static final String GET_UNREAD_CONVERSATION = "108"; + public static final String SET_USER_JWT = "104"; + public static final String UPDATE_USER_HASH = "105"; + public static final String LOG_EVENT_HASH = "106"; + public static final String LOGOUT = "107"; + public static final String SET_LOG_LEVEL = "108"; + public static final String GET_UNREAD_CONVERSATION = "109"; public static final String DISPLAY_MESSENGER = "201"; public static final String DISPLAY_MESSENGER_COMPOSER = "202"; public static final String DISPLAY_CONTENT = "203"; diff --git a/android/src/main/java/com/intercom/reactnative/IntercomModule.java b/android/src/main/java/com/intercom/reactnative/IntercomModule.java index 2b94bab5..81afadc0 100644 --- a/android/src/main/java/com/intercom/reactnative/IntercomModule.java +++ b/android/src/main/java/com/intercom/reactnative/IntercomModule.java @@ -179,6 +179,18 @@ public void setUserHash(String userHash, Promise promise) { } } + @ReactMethod + public void setUserJwt(String userJwt, Promise promise) { + try { + Intercom.client().setUserJwt(userJwt); + promise.resolve(true); + } catch (Exception err) { + Log.e(NAME, "setUserJwt error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.SET_USER_JWT, err.toString()); + } + } + @ReactMethod public void updateUser(ReadableMap params, Promise promise) { UserAttributes userAttributes = IntercomHelpers.buildUserAttributes(params); diff --git a/intercom-react-native.podspec b/intercom-react-native.podspec index 962eab99..a5dd0395 100644 --- a/intercom-react-native.podspec +++ b/intercom-react-native.podspec @@ -20,5 +20,5 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" } s.dependency "React-Core" - s.dependency "Intercom", '~> 18.6.1' + s.dependency "Intercom", '~> 18.2.0' end diff --git a/ios/IntercomModule.m b/ios/IntercomModule.m index 5db7318c..02234de8 100644 --- a/ios/IntercomModule.m +++ b/ios/IntercomModule.m @@ -12,6 +12,7 @@ @implementation IntercomModule NSString *UNIDENTIFIED_REGISTRATION = @"101"; NSString *IDENTIFIED_REGISTRATION = @"102"; NSString *SET_USER_HASH = @"103"; +NSString *SET_USER_JWT = @"106"; NSString *UPDATE_USER = @"104"; NSString *LOG_EVENT = @"105"; NSString *UNREAD_CONVERSATION_COUNT = @"107"; @@ -154,6 +155,17 @@ - (NSData *)dataFromHexString:(NSString *)string { } }; +RCT_EXPORT_METHOD(setUserJwt:(NSString *)userJwt + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + @try { + [Intercom setUserJwt:userJwt]; + resolve(@(YES)); + } @catch (NSException *exception) { + reject(SET_USER_JWT, @"Error in setUserJwt", [self exceptionToError:exception :SET_USER_JWT :@"setUserJwt"]); + } +}; + #pragma mark - Events diff --git a/src/index.tsx b/src/index.tsx index 3bcc776f..a509ba82 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -139,6 +139,19 @@ export type IntercomType = { */ setUserHash(hash: string): Promise; + /** + * Sets a JWT token for the user, necessary for using the Messenger + * when Messenger Security is enforced. This is an improvement to Identity Verification. + * + * Secure your Messenger to make sure that bad actors can't impersonate your users, + * see their conversation history or make unauthorised updates to data. + * + * This should be called before any user login takes place. + * + * @param jwt A JWT token signed with your app's secret key. + */ + setUserJwt(jwt: string): Promise; + /** * Update a user in Intercom with data specified in {@link UserAttributes}. * Full details of the data data attributes that can be stored on a user can be found in {@link UserAttributes}. @@ -303,6 +316,7 @@ const Intercom: IntercomType = { IntercomModule.loginUserWithUserAttributes(userAttributes), logout: () => IntercomModule.logout(), setUserHash: (hash) => IntercomModule.setUserHash(hash), + setUserJwt: (jwt) => IntercomModule.setUserJwt(jwt), updateUser: (userAttributes) => IntercomModule.updateUser(userAttributes), isUserLoggedIn: () => IntercomModule.isUserLoggedIn(), fetchLoggedInUserAttributes: () =>