diff --git a/README.md b/README.md index 39dd3d79..59a5c4a5 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ ## Installation ```sh -$ npm install @intercom/intercom-react-native +npm install @intercom/intercom-react-native ``` or @@ -61,7 +61,7 @@ If you're using React Native v0.60 or above, the library will be linked automati #### Android: Automatic linking with React Native v0.59 and below ``` -$ react-native link @intercom/intercom-react-native +react-native link @intercom/intercom-react-native ``` #### Android: Manual linking with React Native v0.59 and below @@ -81,6 +81,16 @@ implementation project(':intercom-react-native') #### Android: Setup +You have two options for initializing Intercom: + +**Option 1: Native Initialization (Recommended)** +Initialize at app startup in your native code for the best user experience. + +**Option 2: JavaScript Initialization** +Initialize manually from JavaScript for more control over timing. If you choose this option, skip the native initialization code below and see the [`initialize` method documentation](#intercomintializeapikey-appid) for implementation details. + +**For Native Initialization:** + - Add below lines to `android/app/src/main/java/com/YOUR_APP/app/MainApplication.java` inside `onCreate` method, replacing `apiKey` and `appId` which can be found in your [workspace settings](https://app.intercom.com/a/apps/_/settings/android). ```java @@ -340,6 +350,16 @@ See [How to manually link IOS Intercom SDK](docs/IOS-MANUAL-LINKING.md) #### iOS: Setup +You have two options for initializing Intercom: + +**Option 1: Native Initialization (Recommended)** +Initialize at app startup in your native code for the best user experience. + +**Option 2: JavaScript Initialization** +Initialize manually from JavaScript for more control over timing. If you choose this option, skip the native initialization code below and see the [`initialize` method documentation](#intercomintializeapikey-appid) for implementation details with platform-specific API key handling. + +**For Native Initialization:** + - Open `ios/AppDelegate.m` then add below code: - At the top of file add the following: @@ -359,7 +379,7 @@ See [How to manually link IOS Intercom SDK](docs/IOS-MANUAL-LINKING.md) // ... self.window.rootViewController = rootViewController; - [IntercomModule initialize:@"apiKey" withAppId:@"appId"]; // <-- Add this (Remember to replace strings with your api keys) + [IntercomModule initialize:@"apiKey" withAppId:@"appId"]; // <-- Add this return YES; } @@ -384,7 +404,7 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau .... } - ``` +``` #### iOS: Permissions @@ -518,6 +538,7 @@ The plugin provides props for extra customization. Every time you change the pro - `androidApiKey` (_string_): Android API Key from Intercom. - `iosApiKey` (_string_): iOS API Key from Intercom. - `intercomRegion` (_string_): Region for Intercom `US`, `EU`, `AU`. Optional. Defaults to `US`. +- `useManualInit` (_boolean_): Set to `true` to manually initialize Intercom from JavaScript instead of at app startup. Optional. Defaults to `false`. ```json { @@ -537,6 +558,41 @@ The plugin provides props for extra customization. Every time you change the pro } ``` +#### Manual Initialization with Expo + +If you want to delay Intercom initialization and manually initialize it from JavaScript, you set the `useManualInit` option to `true`: + +```json +{ + "expo": { + "plugins": [ + [ + "@intercom/intercom-react-native", + { + "useManualInit": true + } + ] + ] + } +} +``` + +Then initialize Intercom manually in your JavaScript code with the platform-specific API keys: + +```javascript +import Intercom from '@intercom/intercom-react-native'; +import { Platform } from 'react-native'; + +// You can find your API keys in your Intercom workspace settings +// https://app.intercom.com/a/apps//settings/channels/messenger/install?tab=ios +const apiKey = Platform.select({ + ios: 'ios_sdk-abc123', + android: 'android_sdk-abc123', +}); + +await Intercom.initialize(apiKey, 'abc123'); +``` + #### Expo: Push notifications Add the following configurations into your `app.json` or `app.config.js`: @@ -697,6 +753,38 @@ Sets the user hash necessary for validation when Identity Verification is enable --- +### `Intercom.initialize(apiKey, appId)` + +Initialize the Intercom SDK manually. This is useful when you want to delay initialization until after your app has started, or when using Expo with the `useManualInit` plugin option. + +**Important:** This method configures the SDK but does NOT validate your credentials with Intercom's servers. Invalid API keys or App IDs will only be detected when you attempt to use Intercom features (e.g., login, show messenger). The method will return `true` if the SDK is successfully configured, regardless of credential validity. + +### Options + +| Name | Type | Required | Description | +| ------ | ------ | -------- | --------------------------------------- | +| apiKey | string | yes | Your Platform-specific Intercom API key | +| appId | string | yes | Your Intercom App ID | + +### Examples + +```javascript +import { Platform } from 'react-native'; + +const apiKey = Platform.select({ + ios: 'ios_sdk-abc123', + android: 'android_sdk-xyz789', +}); + +await Intercom.initialize(apiKey, 'your_app_id'); +``` + +### Returns + +`Promise` + +--- + ### `Intercom.loginUnidentifiedUser()` Login a unidentified user. @@ -1223,9 +1311,9 @@ Data Connectors (e.g., Fin Actions), which use these in `Authorization: Bearer < ### Options -| Name | Type | Required | Description | -| ---------- | ------------------------------- | -------- | --------------------------------------------------- | -| authTokens | `{ [key: string]: string }` | yes | An object with token names as keys and JWT strings as values | +| Name | Type | Required | Description | +| ---------- | --------------------------- | -------- | ------------------------------------------------------------ | +| authTokens | `{ [key: string]: string }` | yes | An object with token names as keys and JWT strings as values | ### Example diff --git a/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java b/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java index 831b0987..7bde7b81 100644 --- a/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java +++ b/android/src/main/java/com/intercom/reactnative/IntercomErrorCodes.java @@ -11,6 +11,7 @@ public class IntercomErrorCodes { public static final String GET_UNREAD_CONVERSATION = "108"; 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 DISPLAY_MESSENGER = "201"; public static final String DISPLAY_MESSENGER_COMPOSER = "202"; public static final String DISPLAY_CONTENT = "203"; diff --git a/android/src/newarch/IntercomModule.java b/android/src/newarch/IntercomModule.java index 6784ead3..c892bd10 100644 --- a/android/src/newarch/IntercomModule.java +++ b/android/src/newarch/IntercomModule.java @@ -626,6 +626,23 @@ public void onFailure(@NonNull IntercomError intercomError) { } } + @ReactMethod + public void initialize(String apiKey, String appId, Promise promise) { + try { + Activity activity = getCurrentActivity(); + if (activity != null && activity.getApplication() != null) { + IntercomModule.initialize(activity.getApplication(), apiKey, appId); + promise.resolve(true); + } else { + promise.reject(IntercomErrorCodes.INITIALIZE_ERROR, "Activity is null"); + } + } catch (Exception err) { + Log.e(NAME, "initialize error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.INITIALIZE_ERROR, err.toString()); + } + } + @ReactMethod public void setNeedsStatusBarAppearanceUpdate(Promise promise) { // iOS-only method, no-op on Android diff --git a/android/src/oldarch/IntercomModule.java b/android/src/oldarch/IntercomModule.java index 871b4ff8..32afd733 100644 --- a/android/src/oldarch/IntercomModule.java +++ b/android/src/oldarch/IntercomModule.java @@ -603,6 +603,23 @@ public void onFailure(@NonNull IntercomError intercomError) { } } + @ReactMethod + public void initialize(String apiKey, String appId, Promise promise) { + try { + Activity activity = getCurrentActivity(); + if (activity != null && activity.getApplication() != null) { + IntercomModule.initialize(activity.getApplication(), apiKey, appId); + promise.resolve(true); + } else { + promise.reject(IntercomErrorCodes.INITIALIZE_ERROR, "Activity is null"); + } + } catch (Exception err) { + Log.e(NAME, "initialize error:"); + Log.e(NAME, err.toString()); + promise.reject(IntercomErrorCodes.INITIALIZE_ERROR, err.toString()); + } + } + public static synchronized void initialize(Application application, String apiKey, String appId) { String sdkVersion = BuildConfig.INTERCOM_VERSION_NAME; ReactNativeHeaderInterceptor.setReactNativeVersion(application.getApplicationContext(), sdkVersion); diff --git a/examples/expo-example/app.json b/examples/expo-example/app.json index 8137891c..20f479c6 100644 --- a/examples/expo-example/app.json +++ b/examples/expo-example/app.json @@ -39,9 +39,7 @@ [ "@intercom/intercom-react-native", { - "appId": "YOUR_APP_ID", - "androidApiKey": "android_sdk-YOUR_ANDROID_API_KEY", - "iosApiKey": "ios_sdk-YOUR_IOS_API_KEY" + "useManualInit": true } ], "expo-router", diff --git a/examples/expo-example/app/(tabs)/index.tsx b/examples/expo-example/app/(tabs)/index.tsx index 968e543d..d1a74fa4 100644 --- a/examples/expo-example/app/(tabs)/index.tsx +++ b/examples/expo-example/app/(tabs)/index.tsx @@ -1,11 +1,13 @@ -import React from 'react'; -import { SafeAreaView, ScrollView, StatusBar } from 'react-native'; +import React, { useEffect, useState } from 'react'; +import { SafeAreaView, ScrollView, StatusBar, View, Text } from 'react-native'; import * as Notifications from 'expo-notifications'; import { useIntercom } from '../../hooks/useIntercom'; import { useNotifications } from '../../hooks/useNotifications'; +import { INTERCOM_CONFIG } from '../../config/intercom.config'; import Header from '../../components/Header'; +import Intercom from '@intercom/intercom-react-native'; import AuthenticationSection from '../../components/AuthenticationSection'; import MessagingSection from '../../components/MessagingSection'; import ContentSection from '../../components/ContentSection'; @@ -28,6 +30,34 @@ Notifications.setNotificationHandler({ export default function App() { const intercom = useIntercom(); const notifications = useNotifications(); + const [isInitialized, setIsInitialized] = useState(false); + const [initError, setInitError] = useState(null); + + useEffect(() => { + async function initializeIntercom() { + try { + console.log('Initializing Intercom...'); + if (!INTERCOM_CONFIG.apiKey || !INTERCOM_CONFIG.appId) { + console.error('Intercom API key and app ID are required'); + return; + } + if (!isInitialized) { + await Intercom.initialize( + INTERCOM_CONFIG.apiKey, + INTERCOM_CONFIG.appId + ); + setIsInitialized(true); + setInitError(null); + console.log('Intercom initialized successfully'); + } + } catch (error) { + console.error('Failed to initialize Intercom:', error); + setInitError(error instanceof Error ? error.message : 'Unknown error'); + } + } + + initializeIntercom(); + }, [isInitialized]); return ( @@ -41,6 +71,20 @@ export default function App() { /> + + + Intercom Status:{' '} + {initError && ( + Failed: {initError} + )} + {isInitialized ? ( + Initialized + ) : ( + Initializing... + )} + + + /settings/channels/messenger/install?tab=ios + * + * Note: iOS and Android require different API keys. + */ +export const INTERCOM_CONFIG = { + appId: '', // Replace with your Intercom App ID + apiKey: Platform.select({ + ios: 'ios_sdk-', // Replace with your iOS API key + android: 'android_sdk-', // Replace with your Android API key + }) as string, +}; diff --git a/examples/expo-example/pnpm-lock.yaml b/examples/expo-example/pnpm-lock.yaml index fcd0f448..2c8b6071 100644 --- a/examples/expo-example/pnpm-lock.yaml +++ b/examples/expo-example/pnpm-lock.yaml @@ -792,7 +792,7 @@ packages: '@intercom/intercom-react-native@file:../..': resolution: {directory: ../.., type: directory} - engines: {node: '>=18', yarn: '>=3'} + engines: {node: '>=18'} peerDependencies: react: '*' react-native: '*' diff --git a/ios/IntercomModule.m b/ios/IntercomModule.m index da8ba910..af690ed3 100644 --- a/ios/IntercomModule.m +++ b/ios/IntercomModule.m @@ -17,6 +17,7 @@ @implementation IntercomModule NSString *UNREAD_CONVERSATION_COUNT = @"107"; NSString *SET_USER_JWT = @"109"; NSString *SET_AUTH_TOKENS = @"110"; +NSString *INITIALIZE_ERROR = @"111"; NSString *SEND_TOKEN_TO_INTERCOM = @"302"; NSString *FETCH_HELP_CENTER_COLLECTIONS = @"901"; NSString *FETCH_HELP_CENTER_COLLECTION = @"902"; @@ -28,6 +29,19 @@ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } +RCT_EXPORT_METHOD(initialize:(NSString *)apiKey + withAppId:(NSString *)appId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + @try { + [IntercomModule initialize:apiKey withAppId:appId]; + resolve(@(YES)); + } @catch (NSException *exception) { + NSLog(@"initialize error: %@", exception.reason); + reject(INITIALIZE_ERROR, @"Failed to initialize Intercom", [self exceptionToError:exception :INITIALIZE_ERROR :@"initialize"]); + } +} + + (void)initialize:(nonnull NSString *)apiKey withAppId:(nonnull NSString *)appId { NSString *version = @"0"; diff --git a/src/NativeIntercomSpec.ts b/src/NativeIntercomSpec.ts index 22babe5f..6f62da5f 100644 --- a/src/NativeIntercomSpec.ts +++ b/src/NativeIntercomSpec.ts @@ -27,6 +27,7 @@ interface TurboModuleContent { } export interface Spec extends TurboModule { + initialize(apiKey: string, appId: string): Promise; loginUnidentifiedUser(): Promise; loginUserWithUserAttributes(userAttributes: UserAttributes): Promise; logout(): Promise; diff --git a/src/expo-plugins/@types.ts b/src/expo-plugins/@types.ts index cfdda924..db1c377c 100644 --- a/src/expo-plugins/@types.ts +++ b/src/expo-plugins/@types.ts @@ -1,8 +1,24 @@ export type IntercomRegion = 'US' | 'EU' | 'AU'; -export type IntercomPluginProps = { +type BasePluginProps = { + /** Data hosting region for your Intercom workspace. Defaults to 'US' */ + intercomRegion?: IntercomRegion; +}; + +type AutoInitPluginProps = BasePluginProps & { + appId: string; iosApiKey: string; androidApiKey: string; - appId: string; - intercomRegion?: IntercomRegion; + useManualInit?: false | undefined; +}; + +type ManualInitPluginProps = BasePluginProps & { + /** + * When true, prevents automatic SDK initialization at app startup. + * You'll need to manually call Intercom.initialize() in your JavaScript code. + * All initialization parameters (apiKey and appId) should be provided at runtime. + */ + useManualInit: true; }; + +export type IntercomPluginProps = AutoInitPluginProps | ManualInitPluginProps; diff --git a/src/expo-plugins/index.ts b/src/expo-plugins/index.ts index 4678818a..894069b9 100644 --- a/src/expo-plugins/index.ts +++ b/src/expo-plugins/index.ts @@ -37,13 +37,15 @@ const mainApplication: ConfigPlugin = (_config, props) => '' ); - stringContents = appendContentsInsideDeclarationBlock( - stringContents, - 'onCreate', - `IntercomModule.initialize(this, "${props.androidApiKey}", "${ - props.appId - }")${config.modResults.language === 'java' ? ';' : ''}\n` - ); + if (!props.useManualInit) { + stringContents = appendContentsInsideDeclarationBlock( + stringContents, + 'onCreate', + `IntercomModule.initialize(this, "${props.androidApiKey}", "${ + props.appId + }")${config.modResults.language === 'java' ? ';' : ''}\n` + ); + } config.modResults.contents = stringContents; return config; @@ -116,19 +118,21 @@ const appDelegate: ConfigPlugin = (_config, props) => ) .replace(/\s*IntercomModule\.initialize\((.*), withAppId: (.*)\)/g, ''); - stringContents = isSwift - ? insertContentsInsideSwiftFunctionBlock( - stringContents, - 'application(_:didFinishLaunchingWithOptions:)', - `IntercomModule.initialize("${props.iosApiKey}", withAppId: "${props.appId}")`, - { position: 'tailBeforeLastReturn' } - ) - : insertContentsInsideObjcFunctionBlock( - stringContents, - 'application didFinishLaunchingWithOptions:', - `[IntercomModule initialize:@"${props.iosApiKey}" withAppId:@"${props.appId}"];`, - { position: 'tailBeforeLastReturn' } - ); + if (!props.useManualInit) { + stringContents = isSwift + ? insertContentsInsideSwiftFunctionBlock( + stringContents, + 'application(_:didFinishLaunchingWithOptions:)', + `IntercomModule.initialize("${props.iosApiKey}", withAppId: "${props.appId}")`, + { position: 'tailBeforeLastReturn' } + ) + : insertContentsInsideObjcFunctionBlock( + stringContents, + 'application didFinishLaunchingWithOptions:', + `[IntercomModule initialize:@"${props.iosApiKey}" withAppId:@"${props.appId}"];`, + { position: 'tailBeforeLastReturn' } + ); + } config.modResults.contents = stringContents; return config; diff --git a/src/index.tsx b/src/index.tsx index d74e62ec..856152b1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -67,6 +67,28 @@ export enum Space { } export type IntercomType = { + /** + * Initialize Intercom SDK with API key and App ID. + * This allows manual initialization from React Native instead of requiring native code setup. + * + * For platform-specific API keys, use Platform.select: + * ```typescript + * import { Platform } from 'react-native'; + * + * const apiKey = Platform.select({ + * ios: 'ios_sdk-abc123', + * android: 'android_sdk-xyz789', + * }); + * + * await Intercom.initialize(apiKey, 'your_app_id'); + * ``` + * + * @param apiKey Your Intercom API key + * @param appId Your Intercom App ID + * @return {Promise} A promise that resolves to true if initialization succeeds + */ + initialize: (apiKey: string, appId: string) => Promise; + /** * Login a unidentified user. * This is a user that doesn't have any identifiable information such as a `userId` or `email`. @@ -301,6 +323,52 @@ export type IntercomType = { }; const Intercom: IntercomType = { + initialize: (apiKey, appId) => { + if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') { + return Promise.reject( + new Error('Intercom: apiKey is required and must be a string') + ); + } + if (!appId || typeof appId !== 'string' || appId.trim() === '') { + return Promise.reject( + new Error('Intercom: appId is required and must be a string') + ); + } + + const platform = Platform.OS as 'ios' | 'android'; + const platformRules = { + ios: { prefix: 'ios_sdk-', minLength: 48 }, + android: { prefix: 'android_sdk-', minLength: 52 }, + }; + + const rules = platformRules[platform]; + + if (!rules) { + return Promise.reject( + new Error( + `Intercom: Platform "${platform}" is not supported. Only iOS and Android are supported.` + ) + ); + } + + if (!apiKey.startsWith(rules.prefix)) { + return Promise.reject( + new Error( + `Intercom: ${platform} API key must start with "${rules.prefix}"` + ) + ); + } + + if (apiKey.length < rules.minLength) { + return Promise.reject( + new Error( + `Intercom: ${platform} API key must be at least ${rules.minLength} characters long` + ) + ); + } + + return IntercomModule.initialize(apiKey, appId); + }, loginUnidentifiedUser: () => IntercomModule.loginUnidentifiedUser(), loginUserWithUserAttributes: (userAttributes) => IntercomModule.loginUserWithUserAttributes(userAttributes),