Skip to content

Commit f26eda2

Browse files
feat(auth): Implement JWT authentication for Intercom SDK (iOS 19.0.0, Android 17.0.+)
1 parent ae2e1bf commit f26eda2

File tree

9 files changed

+327
-9
lines changed

9 files changed

+327
-9
lines changed

__tests__/intercom-jwt.test.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/**
2+
* Tests for setUserJWT API specifically
3+
*/
4+
5+
describe('Intercom setUserJWT API', () => {
6+
let mockIntercomModule;
7+
let Intercom;
8+
9+
beforeEach(() => {
10+
jest.resetModules();
11+
12+
// Mock React Native
13+
jest.doMock('react-native', () => ({
14+
NativeModules: {
15+
IntercomModule: {
16+
setUserJWT: jest.fn(),
17+
setUserHash: jest.fn(),
18+
loginUserWithUserAttributes: jest.fn(),
19+
logout: jest.fn(),
20+
updateUser: jest.fn(),
21+
isUserLoggedIn: jest.fn(),
22+
},
23+
IntercomEventEmitter: {
24+
UNREAD_COUNT_CHANGE_NOTIFICATION: 'UNREAD_COUNT_CHANGE_NOTIFICATION',
25+
WINDOW_DID_HIDE_NOTIFICATION: 'WINDOW_DID_HIDE_NOTIFICATION',
26+
WINDOW_DID_SHOW_NOTIFICATION: 'WINDOW_DID_SHOW_NOTIFICATION',
27+
HELP_CENTER_WINDOW_DID_SHOW_NOTIFICATION:
28+
'HELP_CENTER_WINDOW_DID_SHOW_NOTIFICATION',
29+
HELP_CENTER_WINDOW_DID_HIDE_NOTIFICATION:
30+
'HELP_CENTER_WINDOW_DID_HIDE_NOTIFICATION',
31+
startEventListener: jest.fn(),
32+
removeEventListener: jest.fn(),
33+
},
34+
},
35+
NativeEventEmitter: jest.fn().mockImplementation(() => ({
36+
addListener: jest.fn().mockReturnValue({
37+
remove: jest.fn(),
38+
}),
39+
})),
40+
Platform: {
41+
OS: 'ios',
42+
select: jest.fn((obj) => obj.ios || obj.default),
43+
},
44+
}));
45+
46+
const { NativeModules } = require('react-native');
47+
mockIntercomModule = NativeModules.IntercomModule;
48+
49+
// Import Intercom after mocking
50+
Intercom = require('../src/index.tsx').default;
51+
});
52+
53+
afterEach(() => {
54+
jest.clearAllMocks();
55+
});
56+
57+
describe('setUserJWT method', () => {
58+
test('should call native setUserJWT with valid JWT', async () => {
59+
const testJWT =
60+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.test';
61+
mockIntercomModule.setUserJWT.mockResolvedValue(true);
62+
63+
const result = await Intercom.setUserJWT(testJWT);
64+
65+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledWith(testJWT);
66+
expect(result).toBe(true);
67+
});
68+
69+
test('should handle JWT authentication errors', async () => {
70+
const invalidJWT = 'invalid.jwt';
71+
const error = new Error('JWT validation failed');
72+
mockIntercomModule.setUserJWT.mockRejectedValue(error);
73+
74+
await expect(Intercom.setUserJWT(invalidJWT)).rejects.toThrow(
75+
'JWT validation failed'
76+
);
77+
});
78+
79+
test('should work with empty JWT string', async () => {
80+
const emptyJWT = '';
81+
mockIntercomModule.setUserJWT.mockResolvedValue(true);
82+
83+
const result = await Intercom.setUserJWT(emptyJWT);
84+
85+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledWith(emptyJWT);
86+
expect(result).toBe(true);
87+
});
88+
89+
test('should handle long JWT tokens', async () => {
90+
const longJWT =
91+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNDU2Nzg5MCIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImN1c3RvbV9hdHRyaWJ1dGVzIjp7InBsYW4iOiJwcmVtaXVtIiwiY29tcGFueSI6IkFjbWUgSW5jIn19.very_long_signature';
92+
mockIntercomModule.setUserJWT.mockResolvedValue(true);
93+
94+
const result = await Intercom.setUserJWT(longJWT);
95+
96+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledWith(longJWT);
97+
expect(result).toBe(true);
98+
});
99+
});
100+
101+
describe('JWT authentication workflow', () => {
102+
test('should set JWT before user login', async () => {
103+
const jwt =
104+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIn0.test';
105+
const userAttributes = { email: '[email protected]' };
106+
107+
mockIntercomModule.setUserJWT.mockResolvedValue(true);
108+
mockIntercomModule.loginUserWithUserAttributes.mockResolvedValue(true);
109+
110+
await Intercom.setUserJWT(jwt);
111+
await Intercom.loginUserWithUserAttributes(userAttributes);
112+
113+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledWith(jwt);
114+
expect(
115+
mockIntercomModule.loginUserWithUserAttributes
116+
).toHaveBeenCalledWith(userAttributes);
117+
// Verify setUserJWT was called first by checking call counts
118+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledTimes(1);
119+
expect(
120+
mockIntercomModule.loginUserWithUserAttributes
121+
).toHaveBeenCalledTimes(1);
122+
});
123+
124+
test('should support both JWT and HMAC methods', async () => {
125+
const jwt =
126+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIn0.test';
127+
const hash = 'hmac_hash_123';
128+
129+
mockIntercomModule.setUserJWT.mockResolvedValue(true);
130+
mockIntercomModule.setUserHash.mockResolvedValue(true);
131+
132+
const jwtResult = await Intercom.setUserJWT(jwt);
133+
const hashResult = await Intercom.setUserHash(hash);
134+
135+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledWith(jwt);
136+
expect(mockIntercomModule.setUserHash).toHaveBeenCalledWith(hash);
137+
expect(jwtResult).toBe(true);
138+
expect(hashResult).toBe(true);
139+
});
140+
141+
test('should handle complete authentication flow', async () => {
142+
const jwt =
143+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIn0.test';
144+
const userAttributes = { userId: '123', email: '[email protected]' };
145+
146+
mockIntercomModule.setUserJWT.mockResolvedValue(true);
147+
mockIntercomModule.loginUserWithUserAttributes.mockResolvedValue(true);
148+
mockIntercomModule.isUserLoggedIn.mockResolvedValue(true);
149+
mockIntercomModule.updateUser.mockResolvedValue(true);
150+
151+
// Set JWT first
152+
await Intercom.setUserJWT(jwt);
153+
154+
// Login user
155+
await Intercom.loginUserWithUserAttributes(userAttributes);
156+
157+
// Check login status
158+
const isLoggedIn = await Intercom.isUserLoggedIn();
159+
160+
// Update user
161+
await Intercom.updateUser({ name: 'Updated Name' });
162+
163+
expect(mockIntercomModule.setUserJWT).toHaveBeenCalledWith(jwt);
164+
expect(
165+
mockIntercomModule.loginUserWithUserAttributes
166+
).toHaveBeenCalledWith(userAttributes);
167+
expect(isLoggedIn).toBe(true);
168+
expect(mockIntercomModule.updateUser).toHaveBeenCalledWith({
169+
name: 'Updated Name',
170+
});
171+
});
172+
});
173+
174+
describe('Error handling', () => {
175+
test('should handle network errors', async () => {
176+
const jwt = 'test.jwt.token';
177+
const networkError = new Error('Network request failed');
178+
mockIntercomModule.setUserJWT.mockRejectedValue(networkError);
179+
180+
await expect(Intercom.setUserJWT(jwt)).rejects.toThrow(
181+
'Network request failed'
182+
);
183+
});
184+
185+
test('should handle invalid JWT format errors', async () => {
186+
const invalidJWT = 'not.a.valid.jwt';
187+
const formatError = new Error('Invalid JWT format');
188+
mockIntercomModule.setUserJWT.mockRejectedValue(formatError);
189+
190+
await expect(Intercom.setUserJWT(invalidJWT)).rejects.toThrow(
191+
'Invalid JWT format'
192+
);
193+
});
194+
195+
test('should handle JWT signature verification errors', async () => {
196+
const jwtWithBadSignature =
197+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzIn0.bad_signature';
198+
const signatureError = new Error('JWT signature verification failed');
199+
mockIntercomModule.setUserJWT.mockRejectedValue(signatureError);
200+
201+
await expect(Intercom.setUserJWT(jwtWithBadSignature)).rejects.toThrow(
202+
'JWT signature verification failed'
203+
);
204+
});
205+
});
206+
});

__tests__/setupTests.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Test setup for React Native mocking
3+
*/
4+
5+
import { NativeModules } from 'react-native';
6+
7+
// Mock the native modules
8+
const mockIntercomModule = {
9+
setUserHash: jest.fn(),
10+
setUserJWT: jest.fn(),
11+
loginUnidentifiedUser: jest.fn(),
12+
loginUserWithUserAttributes: jest.fn(),
13+
logout: jest.fn(),
14+
updateUser: jest.fn(),
15+
isUserLoggedIn: jest.fn(),
16+
fetchLoggedInUserAttributes: jest.fn(),
17+
logEvent: jest.fn(),
18+
presentIntercom: jest.fn(),
19+
presentIntercomSpace: jest.fn(),
20+
presentContent: jest.fn(),
21+
presentMessageComposer: jest.fn(),
22+
getUnreadConversationCount: jest.fn(),
23+
hideIntercom: jest.fn(),
24+
setBottomPadding: jest.fn(),
25+
setInAppMessageVisibility: jest.fn(),
26+
setLauncherVisibility: jest.fn(),
27+
setNeedsStatusBarAppearanceUpdate: jest.fn(),
28+
handlePushMessage: jest.fn(),
29+
sendTokenToIntercom: jest.fn(),
30+
setLogLevel: jest.fn(),
31+
fetchHelpCenterCollections: jest.fn(),
32+
fetchHelpCenterCollection: jest.fn(),
33+
searchHelpCenter: jest.fn(),
34+
};
35+
36+
const mockEventEmitter = {
37+
UNREAD_COUNT_CHANGE_NOTIFICATION: 'UNREAD_COUNT_CHANGE_NOTIFICATION',
38+
WINDOW_DID_HIDE_NOTIFICATION: 'WINDOW_DID_HIDE_NOTIFICATION',
39+
WINDOW_DID_SHOW_NOTIFICATION: 'WINDOW_DID_SHOW_NOTIFICATION',
40+
HELP_CENTER_WINDOW_DID_SHOW_NOTIFICATION:
41+
'HELP_CENTER_WINDOW_DID_SHOW_NOTIFICATION',
42+
HELP_CENTER_WINDOW_DID_HIDE_NOTIFICATION:
43+
'HELP_CENTER_WINDOW_DID_HIDE_NOTIFICATION',
44+
startEventListener: jest.fn(),
45+
removeEventListener: jest.fn(),
46+
};
47+
48+
NativeModules.IntercomModule = mockIntercomModule;
49+
NativeModules.IntercomEventEmitter = mockEventEmitter;
50+
51+
// Mock Platform
52+
const mockPlatform = {
53+
OS: 'ios',
54+
select: jest.fn((obj) => obj.ios || obj.default),
55+
};
56+
57+
jest.doMock('react-native', () => ({
58+
NativeModules: {
59+
IntercomModule: mockIntercomModule,
60+
IntercomEventEmitter: mockEventEmitter,
61+
},
62+
NativeEventEmitter: jest.fn().mockImplementation(() => ({
63+
addListener: jest.fn().mockReturnValue({
64+
remove: jest.fn(),
65+
}),
66+
})),
67+
Platform: mockPlatform,
68+
}));
69+
70+
export { mockIntercomModule, mockEventEmitter, mockPlatform };

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,5 @@ dependencies {
6969
//noinspection GradleDynamicVersion
7070
implementation "com.facebook.react:react-native:+" // From node_modules
7171
implementation "com.google.firebase:firebase-messaging:${safeExtGet('firebaseMessagingVersion', '20.2.+')}"
72-
implementation 'io.intercom.android:intercom-sdk:15.16.+'
72+
implementation 'io.intercom.android:intercom-sdk:17.0.+'
7373
}

android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ public class IntercomErrorCodes {
44
public static final String UNIDENTIFIED_REGISTRATION = "101";
55
public static final String IDENTIFIED_REGISTRATION = "102";
66
public static final String SET_USER_HASH = "103";
7-
public static final String UPDATE_USER_HASH = "104";
8-
public static final String LOG_EVENT_HASH = "105";
9-
public static final String LOGOUT = "106";
10-
public static final String SET_LOG_LEVEL = "107";
11-
public static final String GET_UNREAD_CONVERSATION = "108";
7+
public static final String SET_USER_JWT = "104";
8+
public static final String UPDATE_USER_HASH = "105";
9+
public static final String LOG_EVENT_HASH = "106";
10+
public static final String LOGOUT = "107";
11+
public static final String SET_LOG_LEVEL = "108";
12+
public static final String GET_UNREAD_CONVERSATION = "109";
1213
public static final String DISPLAY_MESSENGER = "201";
1314
public static final String DISPLAY_MESSENGER_COMPOSER = "202";
1415
public static final String DISPLAY_CONTENT = "203";

android/src/main/java/com/intercom/reactnative/IntercomModule.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@ public void setUserHash(String userHash, Promise promise) {
179179
}
180180
}
181181

182+
@ReactMethod
183+
public void setUserJWT(String jwt, Promise promise) {
184+
try {
185+
Intercom.client().setUserJWT(jwt);
186+
promise.resolve(true);
187+
} catch (Exception err) {
188+
Log.e(NAME, "setUserJWT error:");
189+
Log.e(NAME, err.toString());
190+
promise.reject(IntercomErrorCodes.SET_USER_JWT, err.toString());
191+
}
192+
}
193+
182194
@ReactMethod
183195
public void updateUser(ReadableMap params, Promise promise) {
184196
UserAttributes userAttributes = IntercomHelpers.buildUserAttributes(params);

intercom-react-native.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ Pod::Spec.new do |s|
2020
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
2121

2222
s.dependency "React-Core"
23-
s.dependency "Intercom", '~> 18.6.1'
23+
s.dependency "Intercom", '~> 19.0.0'
2424
end

ios/IntercomModule.m

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ @implementation IntercomModule
1212
NSString *UNIDENTIFIED_REGISTRATION = @"101";
1313
NSString *IDENTIFIED_REGISTRATION = @"102";
1414
NSString *SET_USER_HASH = @"103";
15-
NSString *UPDATE_USER = @"104";
16-
NSString *LOG_EVENT = @"105";
15+
NSString *SET_USER_JWT = @"104";
16+
NSString *UPDATE_USER = @"105";
17+
NSString *LOG_EVENT = @"106";
1718
NSString *UNREAD_CONVERSATION_COUNT = @"107";
1819
NSString *SEND_TOKEN_TO_INTERCOM = @"302";
1920
NSString *FETCH_HELP_CENTER_COLLECTIONS = @"901";
@@ -154,6 +155,17 @@ - (NSData *)dataFromHexString:(NSString *)string {
154155
}
155156
};
156157

158+
RCT_EXPORT_METHOD(setUserJWT:(NSString *)jwt
159+
resolver:(RCTPromiseResolveBlock)resolve
160+
rejecter:(RCTPromiseRejectBlock)reject) {
161+
@try {
162+
[Intercom setUserJWT:jwt];
163+
resolve(@(YES));
164+
} @catch (NSException *exception) {
165+
reject(UPDATE_USER, @"Error in setUserJWT", [self exceptionToError:exception :SET_USER_JWT :@"setUserJWT"]);
166+
}
167+
};
168+
157169

158170
#pragma mark - Events
159171

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@
8181
},
8282
"jest": {
8383
"preset": "react-native",
84+
"setupFilesAfterEnv": ["<rootDir>/__tests__/setupTests.js"],
85+
"testPathIgnorePatterns": [
86+
"<rootDir>/__tests__/setupTests.js"
87+
],
8488
"modulePathIgnorePatterns": [
8589
"<rootDir>/example/node_modules",
8690
"<rootDir>/sandboxes/node_modules",

src/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ export type IntercomType = {
139139
*/
140140
setUserHash(hash: string): Promise<boolean>;
141141

142+
/**
143+
* Set JWT string for authenticating users in the Messenger.
144+
* @note This should be called before any user login takes place.
145+
*
146+
* JWT (JSON Web Token) is the recommended method to secure your Messenger. With JWT, you can ensure that
147+
* bad actors can't impersonate your users, see their conversation history, or make unauthorized updates to data.
148+
*
149+
* @see More information on JWT authentication can be found {@link https://www.intercom.com/help/en/articles/10589769-authenticating-users-in-the-messenger-with-json-web-tokens-jwts here}
150+
* @param jwt A JWT string generated by your server using your Intercom secret key.
151+
*/
152+
setUserJWT(jwt: string): Promise<boolean>;
153+
142154
/**
143155
* Update a user in Intercom with data specified in {@link UserAttributes}.
144156
* Full details of the data data attributes that can be stored on a user can be found in {@link UserAttributes}.
@@ -303,6 +315,7 @@ const Intercom: IntercomType = {
303315
IntercomModule.loginUserWithUserAttributes(userAttributes),
304316
logout: () => IntercomModule.logout(),
305317
setUserHash: (hash) => IntercomModule.setUserHash(hash),
318+
setUserJWT: (jwt) => IntercomModule.setUserJWT(jwt),
306319
updateUser: (userAttributes) => IntercomModule.updateUser(userAttributes),
307320
isUserLoggedIn: () => IntercomModule.isUserLoggedIn(),
308321
fetchLoggedInUserAttributes: () =>

0 commit comments

Comments
 (0)