Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,54 @@ 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. 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.

```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<string \| null>` | yes |

### Returns

`Promise<boolean>`

---

### `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<boolean>`

---

### `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
Expand Down
92 changes: 92 additions & 0 deletions __tests__/userJwtProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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) => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
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 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);
await Intercom.removeUserJwtProvider();
await requestJwt('4');

expect(IntercomModule.removeUserJwtProvider).toHaveBeenCalledTimes(1);
expect(provider).not.toHaveBeenCalled();
expect(IntercomModule.completeUserJwtRequest).not.toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,6 +81,7 @@ public String getName() {
public Map<String, Object> getConstants() {
final Map<String, Object> 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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, IntercomUserJwtCallback> 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;
}
}
}
37 changes: 37 additions & 0 deletions android/src/newarch/IntercomModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions android/src/oldarch/IntercomModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 5 additions & 5 deletions examples/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading