From b3bb2623414b6c1cc9ba61da40712505208d25e7 Mon Sep 17 00:00:00 2001 From: Dheepakraj Selvakumar Date: Mon, 12 Oct 2026 11:09:17 +0100 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20update=20Intercom=20SDK=20dependen?= =?UTF-8?q?cies=20(iOS:=2019.8.1=20=E2=86=92=2019.9.0,=20Android:=2018.9.3?= =?UTF-8?q?=20=E2=86=92=2018.10.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the package to 10.8.0 for the new user JWT provider API these releases add. Co-Authored-By: Claude Opus 5.5 --- android/build.gradle | 4 ++-- examples/example/ios/Podfile.lock | 10 +++++----- examples/with-notifications/ios/Podfile.lock | 10 +++++----- intercom-react-native.podspec | 2 +- package.json | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 839ab472..884c7c9f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -84,6 +84,6 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" // From node_modules implementation "com.google.firebase:firebase-messaging:24.1.2" - implementation 'io.intercom.android:intercom-sdk:18.9.3' - implementation 'io.intercom.android:intercom-sdk-ui:18.9.3' + implementation 'io.intercom.android:intercom-sdk:18.10.0' + implementation 'io.intercom.android:intercom-sdk-ui:18.10.0' } diff --git a/examples/example/ios/Podfile.lock b/examples/example/ios/Podfile.lock index e3e4b668..109dda1b 100644 --- a/examples/example/ios/Podfile.lock +++ b/examples/example/ios/Podfile.lock @@ -8,15 +8,15 @@ PODS: - hermes-engine (0.81.1): - hermes-engine/Pre-built (= 0.81.1) - hermes-engine/Pre-built (0.81.1) - - Intercom (19.8.1) - - intercom-react-native (10.7.1): + - Intercom (19.9.0) + - intercom-react-native (10.8.0): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine - - Intercom (~> 19.8.1) + - Intercom (~> 19.9.0) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2560,8 +2560,8 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca - Intercom: 325645361ecde0014b6d1a0dacde2097f6828218 - intercom-react-native: 0b576e71fab9223d0f08bd27e3b3b1577803448f + Intercom: 4a35bcead5f437a8445a7113a0fe4991f36559fc + intercom-react-native: e384d183f9319923f0e472a47fb018508b98093c RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/examples/with-notifications/ios/Podfile.lock b/examples/with-notifications/ios/Podfile.lock index 5841499a..eeb90afa 100644 --- a/examples/with-notifications/ios/Podfile.lock +++ b/examples/with-notifications/ios/Podfile.lock @@ -8,15 +8,15 @@ PODS: - hermes-engine (0.81.1): - hermes-engine/Pre-built (= 0.81.1) - hermes-engine/Pre-built (0.81.1) - - Intercom (19.8.1) - - intercom-react-native (10.7.1): + - Intercom (19.9.0) + - intercom-react-native (10.8.0): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine - - Intercom (~> 19.8.1) + - Intercom (~> 19.9.0) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2773,8 +2773,8 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca - Intercom: 325645361ecde0014b6d1a0dacde2097f6828218 - intercom-react-native: 0b576e71fab9223d0f08bd27e3b3b1577803448f + Intercom: 4a35bcead5f437a8445a7113a0fe4991f36559fc + intercom-react-native: e384d183f9319923f0e472a47fb018508b98093c MMKV: 7b5df6a8bf785c6705cc490c541b9d8a957c4a64 MMKVCore: 3f40b896e9ab522452df9df3ce983471aa2449ba RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f diff --git a/intercom-react-native.podspec b/intercom-react-native.podspec index de5ef671..6c92eede 100644 --- a/intercom-react-native.podspec +++ b/intercom-react-native.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" } - s.dependency "Intercom", '~> 19.8.1' + s.dependency "Intercom", '~> 19.9.0' is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' diff --git a/package.json b/package.json index c2a1ee53..ba4b1d91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@intercom/intercom-react-native", - "version": "10.7.1", + "version": "10.8.0", "description": "React Native wrapper to bridge our iOS and Android SDK", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js", From 83aaa06bd38e9c7b013dba53fa1b24bbdae5b2e6 Mon Sep 17 00:00:00 2001 From: Dheepakraj Selvakumar Date: Mon, 12 Oct 2026 11:09:17 +0100 Subject: [PATCH 2/5] feat: add setUserJwtProvider and removeUserJwtProvider Lets apps hand Intercom a fresh user JWT when the current one is about to expire or has expired, so failed requests are retried instead of leaving the Messenger broken. Native sends JS a request with an ID and JS answers it; requests fail fast when JS can't be reached or the React context is torn down. Co-Authored-By: Claude Opus 5.5 --- README.md | 47 +++++++++++ __tests__/userJwtProvider.test.ts | 77 ++++++++++++++++++ .../reactnative/IntercomErrorCodes.java | 2 + .../reactnative/IntercomEventEmitter.java | 2 + .../IntercomUserJwtProviderBridge.java | 81 +++++++++++++++++++ android/src/newarch/IntercomModule.java | 37 +++++++++ android/src/oldarch/IntercomModule.java | 37 +++++++++ ios/IntercomEventEmitter.h | 2 + ios/IntercomEventEmitter.m | 12 ++- ios/IntercomModule.m | 75 +++++++++++++++++ src/NativeIntercomSpec.ts | 3 + src/index.tsx | 46 ++++++++++- 12 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 __tests__/userJwtProvider.test.ts create mode 100644 android/src/main/java/com/intercom/reactnative/IntercomUserJwtProviderBridge.java diff --git a/README.md b/README.md index 4ff49a49..0985bcb5 100644 --- a/README.md +++ b/README.md @@ -1345,6 +1345,53 @@ This should be called before any user login takes place. --- +### `Intercom.setUserJwtProvider(provider)` + +Registers a provider Intercom calls when it needs a fresh user JWT, because the current one is about to expire or +Intercom rejected a request because it has expired. Requests that hit an expired JWT are retried once with the new +token instead of failing, and open Messenger screens recover without being reopened. + +The provider is only called while the app is in the foreground, and never more than once at a time. Resolve with a +fresh JWT, or with `null` if you can't supply one. Intercom waits up to 10 seconds for an answer. Calling +`setUserJwt` while a request is pending also completes it. + +Register the provider as early as possible after the app starts. Requests that expire before it is registered fail +until a new JWT is set. + +```javascript +Intercom.setUserJwtProvider(async () => { + const response = await fetch('https://your-backend.example.com/intercom-jwt'); + if (!response.ok) { + return null; + } + const { jwt } = await response.json(); + return jwt; +}); +``` + +### Options + +| Name | Type | Required | +| -------- | ------------------------------ | -------- | +| provider | `() => Promise` | yes | + +### Returns + +`Promise` + +--- + +### `Intercom.removeUserJwtProvider()` + +Removes the provider registered with `setUserJwtProvider`. Requests rejected for an expired JWT then fail until a new +JWT is set with `setUserJwt`. + +### Returns + +`Promise` + +--- + ### `Intercom.setAuthTokens(authTokens)` Provide Intercom with your auth tokens which can be used for functionality such as Custom Actions with Data Connectors. You can provide multiple diff --git a/__tests__/userJwtProvider.test.ts b/__tests__/userJwtProvider.test.ts new file mode 100644 index 00000000..75053f70 --- /dev/null +++ b/__tests__/userJwtProvider.test.ts @@ -0,0 +1,77 @@ +import { DeviceEventEmitter } from 'react-native'; + +jest.mock('../src/nativeModules', () => ({ + IntercomModule: { + setUserJwtProvider: jest.fn(() => Promise.resolve(true)), + removeUserJwtProvider: jest.fn(() => Promise.resolve(true)), + completeUserJwtRequest: jest.fn(), + }, + IntercomEventEmitter: { + USER_JWT_REQUESTED_NOTIFICATION: 'IntercomUserJwtRequested', + addListener: jest.fn(), + removeListeners: jest.fn(), + }, +})); + +const { IntercomModule } = jest.requireMock('../src/nativeModules'); +const Intercom = require('../src').default; + +const requestJwt = async (requestId: string) => { + DeviceEventEmitter.emit('IntercomUserJwtRequested', { requestId }); + await new Promise((resolve) => setImmediate(resolve)); +}; + +describe('setUserJwtProvider', () => { + beforeEach(() => jest.clearAllMocks()); + afterEach(() => Intercom.removeUserJwtProvider()); + + test('completes the request with the JWT the provider resolves', async () => { + await Intercom.setUserJwtProvider(() => Promise.resolve('fresh-jwt')); + await requestJwt('1'); + + expect(IntercomModule.setUserJwtProvider).toHaveBeenCalledTimes(1); + expect(IntercomModule.completeUserJwtRequest).toHaveBeenCalledWith( + '1', + 'fresh-jwt' + ); + }); + + test.each([ + ['resolves null', () => Promise.resolve(null)], + ['resolves an empty string', () => Promise.resolve('')], + ['rejects', () => Promise.reject(new Error('offline'))], + ])('fails the request when the provider %s', async (_, provider) => { + await Intercom.setUserJwtProvider(provider); + await requestJwt('2'); + + expect(IntercomModule.completeUserJwtRequest).toHaveBeenCalledWith( + '2', + null + ); + }); + + test('only the latest provider answers', async () => { + const first = jest.fn(() => Promise.resolve('first')); + await Intercom.setUserJwtProvider(first); + await Intercom.setUserJwtProvider(() => Promise.resolve('second')); + await requestJwt('3'); + + expect(first).not.toHaveBeenCalled(); + expect(IntercomModule.completeUserJwtRequest).toHaveBeenCalledTimes(1); + expect(IntercomModule.completeUserJwtRequest).toHaveBeenCalledWith( + '3', + 'second' + ); + }); + + test('stops answering after removeUserJwtProvider', async () => { + const provider = jest.fn(() => Promise.resolve('jwt')); + await Intercom.setUserJwtProvider(provider); + await Intercom.removeUserJwtProvider(); + await requestJwt('4'); + + expect(IntercomModule.removeUserJwtProvider).toHaveBeenCalledTimes(1); + expect(provider).not.toHaveBeenCalled(); + expect(IntercomModule.completeUserJwtRequest).not.toHaveBeenCalled(); + }); +}); diff --git a/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java b/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java index 5e28f01d..80fed2e8 100644 --- a/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java +++ b/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java @@ -12,6 +12,8 @@ public class IntercomErrorCodes { public static final String SET_USER_JWT = "109"; public static final String SET_AUTH_TOKENS = "110"; public static final String INITIALIZE_ERROR = "111"; + public static final String SET_USER_JWT_PROVIDER = "112"; + public static final String REMOVE_USER_JWT_PROVIDER = "113"; 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/IntercomEventEmitter.java b/android/src/main/java/com/intercom/reactnative/IntercomEventEmitter.java index fd272f4b..d5d1da66 100644 --- a/android/src/main/java/com/intercom/reactnative/IntercomEventEmitter.java +++ b/android/src/main/java/com/intercom/reactnative/IntercomEventEmitter.java @@ -23,6 +23,7 @@ public class IntercomEventEmitter extends ReactContextBaseJavaModule { public static final String NAME = "IntercomEventEmitter"; private static final String UNREAD_COUNT_CHANGE_NOTIFICATION = "IntercomUnreadConversationCountDidChangeNotification"; + static final String USER_JWT_REQUESTED_NOTIFICATION = "IntercomUserJwtRequested"; private int activeListenersCount = 0; private final UnreadConversationCountListener unreadConversationCountListener = new UnreadConversationCountListener() { @Override @@ -80,6 +81,7 @@ public String getName() { public Map getConstants() { final Map constants = new HashMap<>(); constants.put("UNREAD_COUNT_CHANGE_NOTIFICATION", UNREAD_COUNT_CHANGE_NOTIFICATION); + constants.put("USER_JWT_REQUESTED_NOTIFICATION", USER_JWT_REQUESTED_NOTIFICATION); return constants; } diff --git a/android/src/main/java/com/intercom/reactnative/IntercomUserJwtProviderBridge.java b/android/src/main/java/com/intercom/reactnative/IntercomUserJwtProviderBridge.java new file mode 100644 index 00000000..eb5992bb --- /dev/null +++ b/android/src/main/java/com/intercom/reactnative/IntercomUserJwtProviderBridge.java @@ -0,0 +1,81 @@ +package com.intercom.reactnative; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import io.intercom.android.sdk.Intercom; +import io.intercom.android.sdk.IntercomUserJwtCallback; + +class IntercomUserJwtProviderBridge { + private static final String TAG = "IntercomUserJwtProvider"; + private static final Map pendingRequests = new ConcurrentHashMap<>(); + private static final AtomicLong nextRequestId = new AtomicLong(); + + static void setProvider(ReactApplicationContext reactContext) { + Intercom.client().setUserJwtProvider(callback -> { + String requestId = String.valueOf(nextRequestId.incrementAndGet()); + pendingRequests.put(requestId, callback); + if (!emitRequest(reactContext, requestId)) { + complete(requestId, null); + } + }); + } + + static void removeProvider() { + Intercom.client().removeUserJwtProvider(); + failPendingRequests(); + } + + static void complete(String requestId, @Nullable String jwt) { + IntercomUserJwtCallback callback = pendingRequests.remove(requestId); + if (callback == null) { + return; + } + if (jwt == null || jwt.isEmpty()) { + callback.onFailure(); + } else { + callback.onSuccess(jwt); + } + } + + static void invalidate() { + try { + removeProvider(); + } catch (Exception err) { + failPendingRequests(); + } + } + + private static void failPendingRequests() { + for (String requestId : pendingRequests.keySet()) { + complete(requestId, null); + } + } + + private static boolean emitRequest(ReactApplicationContext reactContext, String requestId) { + if (!reactContext.hasActiveReactInstance()) { + return false; + } + try { + WritableMap params = Arguments.createMap(); + params.putString("requestId", requestId); + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(IntercomEventEmitter.USER_JWT_REQUESTED_NOTIFICATION, params); + return true; + } catch (Exception err) { + Log.e(TAG, "Unable to request a user JWT: " + err); + return false; + } + } +} diff --git a/android/src/newarch/IntercomModule.java b/android/src/newarch/IntercomModule.java index d220fa83..bd9604b3 100644 --- a/android/src/newarch/IntercomModule.java +++ b/android/src/newarch/IntercomModule.java @@ -607,6 +607,43 @@ public void setUserJwt(String jwt, Promise promise) { } } + @ReactMethod + public void setUserJwtProvider(Promise promise) { + try { + IntercomUserJwtProviderBridge.setProvider(getReactApplicationContext()); + Log.d(NAME, "setUserJwtProvider"); + promise.resolve(true); + } catch (Exception err) { + Log.e(NAME, "setUserJwtProvider error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.SET_USER_JWT_PROVIDER, err.toString()); + } + } + + @ReactMethod + public void removeUserJwtProvider(Promise promise) { + try { + IntercomUserJwtProviderBridge.removeProvider(); + Log.d(NAME, "removeUserJwtProvider"); + promise.resolve(true); + } catch (Exception err) { + Log.e(NAME, "removeUserJwtProvider error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.REMOVE_USER_JWT_PROVIDER, err.toString()); + } + } + + @ReactMethod + public void completeUserJwtRequest(String requestId, @Nullable String jwt) { + IntercomUserJwtProviderBridge.complete(requestId, jwt); + } + + @Override + public void invalidate() { + IntercomUserJwtProviderBridge.invalidate(); + super.invalidate(); + } + @ReactMethod public void setAuthTokens(ReadableMap authTokens, Promise promise) { try { diff --git a/android/src/oldarch/IntercomModule.java b/android/src/oldarch/IntercomModule.java index 09fbacc8..56d0febb 100644 --- a/android/src/oldarch/IntercomModule.java +++ b/android/src/oldarch/IntercomModule.java @@ -585,6 +585,43 @@ public void setUserJwt(String jwt, Promise promise) { } } + @ReactMethod + public void setUserJwtProvider(Promise promise) { + try { + IntercomUserJwtProviderBridge.setProvider(getReactApplicationContext()); + Log.d(NAME, "setUserJwtProvider"); + promise.resolve(true); + } catch (Exception err) { + Log.e(NAME, "setUserJwtProvider error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.SET_USER_JWT_PROVIDER, err.toString()); + } + } + + @ReactMethod + public void removeUserJwtProvider(Promise promise) { + try { + IntercomUserJwtProviderBridge.removeProvider(); + Log.d(NAME, "removeUserJwtProvider"); + promise.resolve(true); + } catch (Exception err) { + Log.e(NAME, "removeUserJwtProvider error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.REMOVE_USER_JWT_PROVIDER, err.toString()); + } + } + + @ReactMethod + public void completeUserJwtRequest(String requestId, @Nullable String jwt) { + IntercomUserJwtProviderBridge.complete(requestId, jwt); + } + + @Override + public void invalidate() { + IntercomUserJwtProviderBridge.invalidate(); + super.invalidate(); + } + @ReactMethod public void setAuthTokens(ReadableMap authTokens, Promise promise) { try { diff --git a/ios/IntercomEventEmitter.h b/ios/IntercomEventEmitter.h index 18171dea..6d60e3d8 100644 --- a/ios/IntercomEventEmitter.h +++ b/ios/IntercomEventEmitter.h @@ -1,4 +1,6 @@ #import +extern NSString *const IntercomUserJwtRequestedNotification; + @interface IntercomEventEmitter : RCTEventEmitter @end diff --git a/ios/IntercomEventEmitter.m b/ios/IntercomEventEmitter.m index 74269aad..09bcae57 100644 --- a/ios/IntercomEventEmitter.m +++ b/ios/IntercomEventEmitter.m @@ -1,6 +1,7 @@ #import "IntercomEventEmitter.h" #import +NSString *const IntercomUserJwtRequestedNotification = @"IntercomUserJwtRequested"; @implementation IntercomEventEmitter @@ -13,13 +14,15 @@ + (BOOL)requiresMainQueueSetup { - (NSDictionary *)constantsToExport { return @{@"UNREAD_COUNT_CHANGE_NOTIFICATION": IntercomUnreadConversationCountDidChangeNotification, @"WINDOW_DID_HIDE_NOTIFICATION": IntercomWindowDidHideNotification, - @"WINDOW_DID_SHOW_NOTIFICATION": IntercomWindowDidShowNotification + @"WINDOW_DID_SHOW_NOTIFICATION": IntercomWindowDidShowNotification, + @"USER_JWT_REQUESTED_NOTIFICATION": IntercomUserJwtRequestedNotification }; } - (NSArray *)supportedEvents { return @[IntercomUnreadConversationCountDidChangeNotification, - IntercomWindowDidHideNotification, IntercomWindowDidShowNotification]; + IntercomWindowDidHideNotification, IntercomWindowDidShowNotification, + IntercomUserJwtRequestedNotification]; } - (void)handleUpdateUnreadCount:(NSNotification *)notification { @@ -36,11 +39,16 @@ - (void)handleWindowShowHideNotification:(NSNotification *)notification { [self sendEventWithName:IntercomWindowDidShowNotification body:@{@"visible": @YES}]; } +- (void)handleUserJwtRequestedNotification:(NSNotification *)notification { + [self sendEventWithName:IntercomUserJwtRequestedNotification body:notification.userInfo]; +} + // Will be called when this module's first listener is added. - (void)startObserving { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUpdateUnreadCount:) name:IntercomUnreadConversationCountDidChangeNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleWindowDidHideNotification:) name:IntercomWindowDidHideNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleWindowShowHideNotification:) name:IntercomWindowDidShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUserJwtRequestedNotification:) name:IntercomUserJwtRequestedNotification object:nil]; } // Will be called when this module's last listener is removed, or on dealloc. diff --git a/ios/IntercomModule.m b/ios/IntercomModule.m index 60fe0086..036667bd 100644 --- a/ios/IntercomModule.m +++ b/ios/IntercomModule.m @@ -1,6 +1,7 @@ #import "IntercomModule.h" #import "IntercomAttributesBuilder.h" #import "IntercomHelpCenterHelpers.h" +#import "IntercomEventEmitter.h" #import @import Intercom; @@ -8,6 +9,10 @@ @interface Intercom (Intercom) + (void)setReactNativeVersion:(NSString *)v; @end +@interface IntercomModule () +@property (nonatomic, strong) NSMutableDictionary *pendingUserJwtRequests; +@end + @implementation IntercomModule NSString *UNIDENTIFIED_REGISTRATION = @"101"; NSString *IDENTIFIED_REGISTRATION = @"102"; @@ -18,6 +23,8 @@ @implementation IntercomModule NSString *SET_USER_JWT = @"109"; NSString *SET_AUTH_TOKENS = @"110"; NSString *INITIALIZE_ERROR = @"111"; +NSString *SET_USER_JWT_PROVIDER = @"112"; +NSString *REMOVE_USER_JWT_PROVIDER = @"113"; NSString *SEND_TOKEN_TO_INTERCOM = @"302"; NSString *FETCH_HELP_CENTER_COLLECTIONS = @"901"; NSString *FETCH_HELP_CENTER_COLLECTION = @"902"; @@ -346,6 +353,74 @@ - (NSData *)dataFromHexString:(NSString *)string { } }; +RCT_EXPORT_METHOD(setUserJwtProvider:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + @try { + __weak IntercomModule *weakSelf = self; + [Intercom setUserJwtProvider:^(void (^completion)(NSString *_Nullable)) { + dispatch_async(dispatch_get_main_queue(), ^{ + IntercomModule *strongSelf = weakSelf; + if (!strongSelf) { + completion(nil); + return; + } + NSString *requestId = [[NSUUID UUID] UUIDString]; + strongSelf.pendingUserJwtRequests[requestId] = completion; + [[NSNotificationCenter defaultCenter] postNotificationName:IntercomUserJwtRequestedNotification + object:nil + userInfo:@{@"requestId": requestId}]; + }); + }]; + resolve(@(YES)); + } @catch (NSException *exception) { + reject(SET_USER_JWT_PROVIDER, @"Error in setUserJwtProvider", [self exceptionToError:exception :SET_USER_JWT_PROVIDER :@"setUserJwtProvider"]); + } +}; + +RCT_EXPORT_METHOD(removeUserJwtProvider:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + @try { + [Intercom removeUserJwtProvider]; + [self failPendingUserJwtRequests]; + resolve(@(YES)); + } @catch (NSException *exception) { + reject(REMOVE_USER_JWT_PROVIDER, @"Error in removeUserJwtProvider", [self exceptionToError:exception :REMOVE_USER_JWT_PROVIDER :@"removeUserJwtProvider"]); + } +}; + +RCT_EXPORT_METHOD(completeUserJwtRequest:(NSString *)requestId + jwt:(nullable NSString *)jwt) { + void (^completion)(NSString *_Nullable) = self.pendingUserJwtRequests[requestId]; + if (!completion) { + return; + } + [self.pendingUserJwtRequests removeObjectForKey:requestId]; + BOOL hasJwt = [jwt isKindOfClass:[NSString class]] && jwt.length > 0; + completion(hasJwt ? jwt : nil); +}; + +- (NSMutableDictionary *)pendingUserJwtRequests { + if (!_pendingUserJwtRequests) { + _pendingUserJwtRequests = [NSMutableDictionary dictionary]; + } + return _pendingUserJwtRequests; +} + +- (void)failPendingUserJwtRequests { + NSArray *completions = self.pendingUserJwtRequests.allValues; + [self.pendingUserJwtRequests removeAllObjects]; + for (void (^completion)(NSString *_Nullable) in completions) { + completion(nil); + } +} + +- (void)invalidate { + dispatch_async(dispatch_get_main_queue(), ^{ + [Intercom removeUserJwtProvider]; + [self failPendingUserJwtRequests]; + }); +} + RCT_EXPORT_METHOD(setAuthTokens:(NSDictionary *)authTokens resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/src/NativeIntercomSpec.ts b/src/NativeIntercomSpec.ts index 948eb6fd..5dedc99c 100644 --- a/src/NativeIntercomSpec.ts +++ b/src/NativeIntercomSpec.ts @@ -85,6 +85,9 @@ export interface Spec extends TurboModule { setLogLevel(logLevel: string): Promise; setThemeMode(themeMode: string): Promise; setUserJwt(jwt: string): Promise; + setUserJwtProvider(): Promise; + removeUserJwtProvider(): Promise; + completeUserJwtRequest(requestId: string, jwt: string | null): void; setAuthTokens(authTokens: Object): Promise; } diff --git a/src/index.tsx b/src/index.tsx index dd7e600a..ae81793a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,5 @@ -import { Platform } from 'react-native'; +import { NativeEventEmitter, Platform } from 'react-native'; +import type { EmitterSubscription } from 'react-native'; import { IntercomModule, IntercomEventEmitter } from './nativeModules'; import type { MetaData, @@ -298,6 +299,25 @@ export type IntercomType = { */ setUserJwt(jwt: string): Promise; + /** + * Registers a provider Intercom calls when it needs a fresh user JWT, because the current one is + * about to expire or Intercom rejected a request because it has expired. Requests that hit an + * expired JWT are retried once with the new token instead of failing. + * + * The provider is only called while the app is in the foreground, and never more than once at a time. + * Resolve with a fresh JWT, or with `null` if you can't supply one. Intercom waits up to 10 seconds. + * Calling `setUserJwt` while a request is pending also completes it. + * + * @param provider A function that resolves to a fresh JWT, or `null`. + */ + setUserJwtProvider(provider: () => Promise): Promise; + + /** + * Removes the provider registered with `setUserJwtProvider`. Requests rejected for an expired JWT + * then fail until a new JWT is set with `setUserJwt`. + */ + removeUserJwtProvider(): Promise; + /** * Provide Intercom with your auth tokens which can be used for functionality * such as Custom Actions with Data Connectors. You can provide multiple tokens at once. To create tokens: @@ -321,6 +341,8 @@ export type IntercomType = { bootstrapEventListeners: () => () => void; }; +let userJwtRequestSubscription: EmitterSubscription | undefined; + const Intercom: IntercomType = { initialize: (apiKey, appId) => { if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') { @@ -411,6 +433,28 @@ const Intercom: IntercomType = { setLogLevel: (logLevel) => IntercomModule.setLogLevel(logLevel), setThemeMode: (themeMode) => IntercomModule.setThemeMode(themeMode), setUserJwt: (jwt) => IntercomModule.setUserJwt(jwt), + setUserJwtProvider: (provider) => { + userJwtRequestSubscription?.remove(); + userJwtRequestSubscription = new NativeEventEmitter( + IntercomEventEmitter + ).addListener( + IntercomEventEmitter.USER_JWT_REQUESTED_NOTIFICATION, + async ({ requestId }: { requestId: string }) => { + let jwt: string | null = null; + try { + const result = await provider(); + jwt = typeof result === 'string' && result.length > 0 ? result : null; + } catch {} + IntercomModule.completeUserJwtRequest(requestId, jwt); + } + ); + return IntercomModule.setUserJwtProvider(); + }, + removeUserJwtProvider: () => { + userJwtRequestSubscription?.remove(); + userJwtRequestSubscription = undefined; + return IntercomModule.removeUserJwtProvider(); + }, setAuthTokens: (authTokens) => IntercomModule.setAuthTokens(authTokens), bootstrapEventListeners: () => { From cfd99fe994e88890cd6304cc5312f659ad77b755 Mon Sep 17 00:00:00 2001 From: Dheepakraj Selvakumar Date: Mon, 12 Oct 2026 15:03:35 +0100 Subject: [PATCH 3/5] fix: tidy up a failed setUserJwtProvider and log provider errors Stops listening for JWT requests when native rejects the registration, warns when the provider throws instead of failing silently, and notes the 30s cool-down after a failed or slow provider in the README. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +++-- __tests__/userJwtProvider.test.ts | 15 +++++++++++++++ src/index.tsx | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0985bcb5..0233def2 100644 --- a/README.md +++ b/README.md @@ -1352,8 +1352,9 @@ Intercom rejected a request because it has expired. Requests that hit an expired token instead of failing, and open Messenger screens recover without being reopened. The provider is only called while the app is in the foreground, and never more than once at a time. Resolve with a -fresh JWT, or with `null` if you can't supply one. Intercom waits up to 10 seconds for an answer. Calling -`setUserJwt` while a request is pending also completes it. +fresh JWT, or with `null` if you can't supply one. Intercom waits up to 10 seconds for an answer. If the provider +fails or doesn't answer in time, Intercom waits 30 seconds before asking again. Calling `setUserJwt` while a request +is pending also completes it. Register the provider as early as possible after the app starts. Requests that expire before it is registered fail until a new JWT is set. diff --git a/__tests__/userJwtProvider.test.ts b/__tests__/userJwtProvider.test.ts index 75053f70..b9ada630 100644 --- a/__tests__/userJwtProvider.test.ts +++ b/__tests__/userJwtProvider.test.ts @@ -41,6 +41,7 @@ describe('setUserJwtProvider', () => { ['resolves an empty string', () => Promise.resolve('')], ['rejects', () => Promise.reject(new Error('offline'))], ])('fails the request when the provider %s', async (_, provider) => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); await Intercom.setUserJwtProvider(provider); await requestJwt('2'); @@ -64,6 +65,20 @@ describe('setUserJwtProvider', () => { ); }); + test('stops listening when native rejects the registration', async () => { + const provider = jest.fn(() => Promise.resolve('jwt')); + IntercomModule.setUserJwtProvider.mockImplementationOnce(() => + Promise.reject(new Error('Intercom has been initialized incorrectly')) + ); + await expect(Intercom.setUserJwtProvider(provider)).rejects.toThrow( + 'initialized incorrectly' + ); + await requestJwt('5'); + + expect(provider).not.toHaveBeenCalled(); + expect(IntercomModule.completeUserJwtRequest).not.toHaveBeenCalled(); + }); + test('stops answering after removeUserJwtProvider', async () => { const provider = jest.fn(() => Promise.resolve('jwt')); await Intercom.setUserJwtProvider(provider); diff --git a/src/index.tsx b/src/index.tsx index ae81793a..046a2e22 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -435,7 +435,7 @@ const Intercom: IntercomType = { setUserJwt: (jwt) => IntercomModule.setUserJwt(jwt), setUserJwtProvider: (provider) => { userJwtRequestSubscription?.remove(); - userJwtRequestSubscription = new NativeEventEmitter( + const subscription = new NativeEventEmitter( IntercomEventEmitter ).addListener( IntercomEventEmitter.USER_JWT_REQUESTED_NOTIFICATION, @@ -444,11 +444,20 @@ const Intercom: IntercomType = { try { const result = await provider(); jwt = typeof result === 'string' && result.length > 0 ? result : null; - } catch {} + } catch (error) { + console.warn('Intercom: user JWT provider threw an error', error); + } IntercomModule.completeUserJwtRequest(requestId, jwt); } ); - return IntercomModule.setUserJwtProvider(); + userJwtRequestSubscription = subscription; + return IntercomModule.setUserJwtProvider().catch((error: unknown) => { + subscription.remove(); + if (userJwtRequestSubscription === subscription) { + userJwtRequestSubscription = undefined; + } + throw error; + }); }, removeUserJwtProvider: () => { userJwtRequestSubscription?.remove(); From defdace867c0ddb87ddf3b28e023900161a58fd1 Mon Sep 17 00:00:00 2001 From: Dheepakraj Selvakumar Date: Mon, 12 Oct 2026 15:03:36 +0100 Subject: [PATCH 4/5] chore: add a user JWT provider demo to the example app Lets the example set a user JWT and register a provider that returns a staged JWT, with a counter of provider calls, to exercise the new API by hand. Co-Authored-By: Claude Opus 5.5 --- examples/example/src/App.tsx | 71 +++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/examples/example/src/App.tsx b/examples/example/src/App.tsx index 6c98df6f..078f4bd4 100644 --- a/examples/example/src/App.tsx +++ b/examples/example/src/App.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Alert, Image, @@ -51,6 +51,11 @@ export default function App() { const [launcherVisibility, setLauncherVisibility] = useState(false); const [user, setUser] = useState({ email: '' }); const [currentTheme, setCurrentTheme] = useState(ThemeMode.SYSTEM); + const [userJwt, setUserJwt] = useState(''); + const [providerJwt, setProviderJwt] = useState(''); + const providerJwtRef = useRef(''); + const [jwtProviderEnabled, setJwtProviderEnabled] = useState(false); + const [jwtProviderCalls, setJwtProviderCalls] = useState(0); const [conversationId, setConversationId] = useState( CONVERSATION_ID @@ -192,6 +197,11 @@ export default function App() { {`Logged In: ${loggedUser ? 'Yes' : 'No'}`} Unread messages count: {count} + + {`JWT provider: ${ + jwtProviderEnabled ? 'On' : 'Off' + } (calls: ${jwtProviderCalls})`} +