RealTimeX
Sign inStart for free
RealTimeXMobile Package
Introduction5-minute overviewFeaturesHow the package worksGlossary
Requirements & dependenciesBackend credentialsInstallation step by stepTutorial — your first chat appProject config fileApp entry — index.jsQuick start — minimum working chatComplete ChatScreen example
Native configuration (iOS & Android)Firebase, APNs & backend setup
Navigation — embedding the chatSession, auth & Socket.IOPermissions & access controlAudio & video callingPush notificationsMentions viewRich-text messages (WebView editor)Total unread count
Customization — four waysDefault component fallback (important)Which slots are wiredCustomization examples (Case 1–4)UI feature flags & copy (ui)Chat info (group + DM)Style overrides (ui.styles)Render props (ChatProvider)Theme, fonts & appearance
ChatProvider props referenceconfigureChat options referenceHooks & storeCallbacks referenceAPI query paramsAll exports
Production integration pattern (Fieldr)Host app integration — providers, toasts, tab bars & navigationEnvironment variablesLocal package / monorepo (Metro)
TroubleshootingSetup checklistArchitectureDocumentation & file mapPackage development (maintainers)License
Mobile packageIntegration Guide

Production integration pattern (Fieldr)

Fieldr (~/Projects/fieldr-2.0-mobile) is the reference host app: chat in a bottom tab, job / customer chat rooms opened from other screens, calls and push from any screen. Its structure, file by file:

code
1fieldr-2.0-mobile/2├── index.js                                    # VideoSDK register + push options + background handlers3├── metro.config.js                             # local portal: install of the package4├── babel.config.js                             # react-native-worklets/plugin5├── patches/@videosdk.live+react-native-webrtc+0.0.22.patch6├── android/app/src/main/AndroidManifest.xml    # supportsPictureInPicture on MainActivity7├── ios/Podfile                                 # setup_permissions + FieldrBroadcast target8├── ios/Fieldr/Info.plist                       # usage strings, RTC keys, background modes9├── ios/Fieldr/AppDelegate.swift                # FirebaseApp.configure()10├── ios/FieldrBroadcast/                        # SampleHandler.swift, Info.plist, FieldrBroadcast.entitlements11└── src/12    ├── navigation/rootNavigation.tsx           # ActiveCallScreen route + <ChatSessionHost />13    ├── screens/chat/14    │   ├── ChatScreen.tsx                      # Chat tab: ChatProvider + ChatNavigator15    │   ├── ChatSessionHost.tsx                 # app-wide session + CallProvider16    │   ├── chatNavigatorPresence.ts            # "is a ChatNavigator mounted?" registry17    │   └── scoped/18    │       ├── scopedChat.ts                   # URLs, users query params, job chat tags19    │       ├── useScopedChatConfig.ts          # config without ui.styles20    │       ├── ScopedChatList.tsx              # job/customer chat list (exported ConversationListItem)21    │       └── ScopedChatRoomScreen.tsx        # one room, opened from a job22    └── utils/23        ├── GlobalNotificationListener.tsx      # skips + forwards RealtimeX pushes24        ├── pushDevice.ts                       # logoutChatSession() on logout25        └── toastConfig.tsx                     # `notification` toast for the in-app banner

Integration order: package.json → metro.config.js → babel.config.js → native files → index.js → rootNavigation.tsx + ChatSessionHost.tsx → ChatScreen.tsx → notification listener, logout and toast.

package.json — dependency

json
1{2  "dependencies": {3    "@realtimexsco/live-chat-react-native": "portal:/Users/apple/Projects/realtimex-npm-package-mobile",4    "@notifee/react-native": "^9.1.8",5    "@react-native-firebase/app": "^25.1.0",6    "@react-native-firebase/messaging": "^25.1.0",7    "react-native-callkeep": "^4.3.16",8    "react-native-voip-push-notification": "^3.3.3",9    "@videosdk.live/react-native-sdk": "^0.6.0"10  },11  "scripts": {"postinstall": "patch-package"}12}

Published apps use a version range ("^1.1.3") instead of portal:.

metro.config.js — local package

Needed only because portal: symlinks the package. See Local package / monorepo for the full file and why each key exists.

src/navigation/rootNavigation.tsx — call route + session host

ActiveCallScreen lives in the root stack so a call can open over any screen, and ChatSessionHost sits inside the NavigationContainer:

tsxsrc/navigation/rootNavigation.tsx
1// src/navigation/rootNavigation.tsx (excerpt)2import {ActiveCallScreen} from '@realtimexsco/live-chat-react-native';3import ChatSessionHost from '../screens/chat/ChatSessionHost';4 5<NavigationContainer ref={navigationRef}>6  <Stack.Navigator>7    {/* …app screens… */}8    <Stack.Screen9      name="ActiveCallScreen"10      component={ActiveCallScreen}11      options={{gestureEnabled: false}}12    />13  </Stack.Navigator>14  <ChatSessionHost />15</NavigationContainer>

src/screens/chat/ChatSessionHost.tsx — app-wide session

Connects the socket (presence, incoming calls) from app start, whichever tab is open. It renders CallProvider only while no ChatNavigator is mounted, because ChatNavigator brings its own — two would each show the incoming-call overlay.

tsxsrc/screens/chat/ChatSessionHost.tsx
1// src/screens/chat/ChatSessionHost.tsx (permission checks trimmed)2import {3  bootstrapChatSession,4  CallProvider,5  ChatProvider,6  connectChatSocket,7  useCallStore,8} from '@realtimexsco/live-chat-react-native';9import React, {useEffect} from 'react';10import Config from 'react-native-config';11import {useSelector} from 'react-redux';12import {navigationRef} from '../../navigation/navigationUtils';13import {selectUser} from '../auth/redux/selectors';14import {focusMountedChatNavigator, useIsChatNavigatorMounted} from './chatNavigatorPresence';15import {selectChatAccessToken} from './redux/selectors';16import {useScopedChatConfig} from './scoped/useScopedChatConfig';17 18const CALL_SCREEN_STATUSES = new Set(['creating', 'ringing-out', 'in-call']);19const CALLKIT_CALL_STATUSES = new Set(['incoming', 'joining', 'in-call']);20 21const ChatSessionHost = () => {22  const userData: any = useSelector(selectUser);23  const chatAccessToken = useSelector(selectChatAccessToken);24  const chatConfig = useScopedChatConfig();25  const chatNavigatorMounted = useIsChatNavigatorMounted();26  const uiStatus = useCallStore(state => state.uiStatus);27 28  useEffect(() => {29    if (!chatAccessToken) return;30    connectChatSocket(chatAccessToken, Config.chatClientId);31    const timer = setTimeout(() => {32      void bootstrapChatSession({33        name: userData?.name,34        email: userData?.email,35        clientId: Config.chatClientId,36        hostAccessToken: chatAccessToken,37      });38    }, 0);39    return () => clearTimeout(timer);40  }, [chatAccessToken, userData?.name, userData?.email]);41 42  // A mounted ChatNavigator opens the call screen in its own stack — bring it to the top.43  useEffect(() => {44    if (chatNavigatorMounted && CALL_SCREEN_STATUSES.has(uiStatus)) {45      focusMountedChatNavigator();46    }47  }, [chatNavigatorMounted, uiStatus]);48 49  if (!chatConfig) {50    // A call answered from CallKit on a cold start runs before login is restored.51    return CALLKIT_CALL_STATUSES.has(uiStatus) ? (52      <CallProvider navigationRef={navigationRef as any}>{null}</CallProvider>53    ) : null;54  }55 56  return (57    <ChatProvider58      config={chatConfig}59      autoConnect={false}60      withSafeArea={false}61      withGestureRoot={false}62      withToast={false}>63      {chatNavigatorMounted ? null : (64        <CallProvider navigationRef={navigationRef as any}>{null}</CallProvider>65      )}66    </ChatProvider>67  );68};69 70export default ChatSessionHost;

chatNavigatorPresence.ts is a tiny useSyncExternalStore registry: each screen that mounts a ChatNavigator calls useRegisterChatNavigator(active, focus), and useIsChatNavigatorMounted() tells the host whether any is mounted.

src/screens/chat/ChatScreen.tsx — the Chat tab

tsxsrc/screens/chat/ChatScreen.tsx
1// src/screens/chat/ChatScreen.tsx (permission checks trimmed)2import {3  bootstrapChatSession,4  ChatNavigator,5  ChatProvider,6  connectChatSocket,7} from '@realtimexsco/live-chat-react-native';8import React, {useEffect, useMemo} from 'react';9import {StyleSheet, View} from 'react-native';10import Config from 'react-native-config';11import {useSelector} from 'react-redux';12import {useTheme} from '../../utils/ThemeContext';13import {popToRootRoute} from '../../navigation/navigationUtils';14import {selectUser} from '../auth/redux/selectors';15import {useRegisterChatNavigator} from './chatNavigatorPresence';16import {selectChatAccessToken} from './redux/selectors';17import {CHAT_USERS_QUERY_PARAMS} from './scoped/scopedChat';18 19const ChatScreen = () => {20  const userData: any = useSelector(selectUser);21  const chatAccessToken = useSelector(selectChatAccessToken);22  const {theme} = useTheme();23  const showChatFab = true; // Fieldr: module permission + entitlement checks24 25  const chatConfig = useMemo(() => {26    if (!chatAccessToken) return undefined;27    connectChatSocket(chatAccessToken, Config.chatClientId);28    return {29      apiUrl: 'https://realtimex.softwareco.com/api/v1',30      socketUrl: 'https://realtimex.softwareco.com',31      clientId: Config.chatClientId,32      accessToken: chatAccessToken,33      callUri: 'https://realtimex.softwareco.com/api/v2',34      queryParamsByApi: {users: CHAT_USERS_QUERY_PARAMS},35      fonts: {Regular: 'MyApp-Regular', SemiBold: 'MyApp-SemiBold', Bold: 'MyApp-Bold'},36      theme: {colorMode: 'light' as const, accent: theme.primary},37      ui: {38        styles: {39          conversationList: {40            // hide the FAB for users without write access41            fabWrap: showChatFab42              ? {right: 24}43              : {display: 'none', height: 0, width: 0, opacity: 0, overflow: 'hidden'},44          },45        },46        conversationList: {47          title: 'Messages',48          fabBottomInset: 70, // must match the tab bar height (tab bar overlays the screen)49          showNewChatFab: true,50        },51        chatRoom: {header: {showVideoCall: true, showVoiceCall: true, showSearch: true}},52      },53    };54  }, [chatAccessToken, showChatFab, theme.primary]);55 56  useEffect(() => {57    if (!chatAccessToken) return;58    const timer = setTimeout(() => {59      void bootstrapChatSession({60        name: userData?.name,61        email: userData?.email,62        clientId: Config.chatClientId,63        hostAccessToken: chatAccessToken,64      });65    }, 0);66    return () => clearTimeout(timer);67  }, [chatAccessToken, userData?.name, userData?.email]);68 69  useRegisterChatNavigator(!!chatConfig, () =>70    popToRootRoute('BottomTab', {screen: 'ChatScreen'}),71  );72 73  if (!chatConfig) return null;74 75  return (76    <View style={styles.root}>77      <ChatProvider config={chatConfig} autoConnect={false} withSafeArea={false} withToast={false}>78        <ChatNavigator withContainer={false} />79      </ChatProvider>80    </View>81  );82};83 84export default ChatScreen;85 86const styles = StyleSheet.create({root: {flex: 1}});

Why each setting:

SettingReason
autoConnect={false} + bootstrapChatSession({name, email, clientId, hostAccessToken})Fieldr passes identity explicitly; the bootstrap runs after first paint (setTimeout(0))
withSafeArea={false}, withToast={false}Fieldr's root already has SafeAreaProvider and <Toast config={toastConfig} />
ChatNavigator withContainer={false}Nested in Fieldr's own NavigationContainer
fabBottomInset: 70Fieldr's tab bar floats over the screen
ui.styles.conversationList.fabWrapHides the FAB for users without write permission. Only style keys belong in ui.styles — title and showNewChatFab go in ui.conversationList

src/screens/chat/scoped/useScopedChatConfig.ts — same config without styles

The package keeps one global config and merges styles.conversationList shallowly. A style set by a job chat room would stick to the main Chat tab, so the scoped config omits ui.styles:

typescriptsrc/screens/chat/scoped/useScopedChatConfig.ts
1// src/screens/chat/scoped/useScopedChatConfig.ts2import {useMemo} from 'react';3import Config from 'react-native-config';4import {useSelector} from 'react-redux';5import {useTheme} from '../../../utils/ThemeContext';6import {selectChatAccessToken} from '../redux/selectors';7import {CHAT_API_URL, CHAT_SOCKET_URL, CHAT_USERS_QUERY_PARAMS} from './scopedChat';8 9export const useScopedChatConfig = () => {10  const chatAccessToken = useSelector(selectChatAccessToken);11  const {theme}: any = useTheme();12 13  return useMemo(() => {14    if (!chatAccessToken) return undefined;15    return {16      apiUrl: CHAT_API_URL,17      socketUrl: CHAT_SOCKET_URL,18      clientId: Config.chatClientId,19      accessToken: chatAccessToken,20      queryParamsByApi: {users: CHAT_USERS_QUERY_PARAMS},21      fonts: {Regular: 'MyApp-Regular', SemiBold: 'MyApp-SemiBold', Bold: 'MyApp-Bold'},22      theme: {colorMode: 'light' as const, accent: theme.primary},23      ui: {chatRoom: {header: {showVideoCall: true, showVoiceCall: true, showSearch: true}}},24    };25  }, [chatAccessToken, theme.primary]);26};

scopedChat.ts holds CHAT_API_URL, CHAT_SOCKET_URL and CHAT_USERS_QUERY_PARAMS = {'metadata.workspaceIds': '…'} (extra params for GET /user/list, so new-chat and add-member pickers only show that workspace's users).

src/screens/chat/scoped/ScopedChatRoomScreen.tsx — one room from another screen

Opens the package stack directly on a room. It waits for useChatSession().isSessionReady and loggedUserId before mounting, because the room loads messages on mount:

tsx
1// opening it2navigation.navigate('ScopedChatRoomScreen', {3  screen: 'ChatRoomScreen',4  params: roomParams(row, meId), // ChatRoomParams5});6 7// inside ScopedChatRoomScreen.tsx8const {isSessionReady, loggedUserId} = useChatSession();9if (!chatConfig || !isSessionReady || !loggedUserId) return <Spinner />;10 11return (12  <ChatProvider config={chatConfig} autoConnect={false} withSafeArea={false} withToast={false}>13    <ChatNavigator withContainer={false} />14  </ChatProvider>15);

src/utils/GlobalNotificationListener.tsx — host push listener

Fieldr has its own FCM / Notifee listeners for non-chat pushes. They skip RealtimeX pushes (the package shows and routes them) and forward the launch notification, because FCM and Notifee hand it to the first caller only:

tsx
1import {2  extractPushData,3  handleRealtimexNotifeeEvent,4  isRealtimexPush,5  openRealtimexNotification,6} from '@realtimexsco/live-chat-react-native';7 8const isChatPush = (message: any) => isRealtimexPush(extractPushData(message));9 10const forwardInitialChatFcm = (message: any) => {11  void openRealtimexNotification(extractPushData(message));12};13 14const forwardInitialChatNotifee = (notification: any, pressAction?: any) => {15  const isAction = !!pressAction?.id && pressAction.id !== 'default';16  void handleRealtimexNotifeeEvent({17    type: isAction ? EventType.ACTION_PRESS : EventType.PRESS,18    detail: {notification, pressAction},19  });20};21 22// in onMessage / onNotificationOpenedApp:   if (isChatPush(message)) return;23// in getInitialNotification():              if (isChatPush(message)) forwardInitialChatFcm(message);

src/utils/pushDevice.ts — logout

logoutChatSession() deletes the chat push tokens server-side, so call it while the token is still valid, with a timeout so logout never hangs:

typescript
1import {logoutChatSession} from '@realtimexsco/live-chat-react-native';2 3const withTimeout = <T,>(promise: Promise<T>, ms: number) =>4  Promise.race([promise, new Promise<void>(resolve => setTimeout(resolve, ms))]);5 6export const unregisterPushDeviceBestEffort = async () => {7  // …remove the host's own device token…8  try {9    await withTimeout(logoutChatSession(), 2000);10  } catch (error) {11    console.log('logoutChatSession error', error);12  }13};

src/utils/toastConfig.tsx — in-app banner

With withToast={false} the package calls Toast.show({type: 'notification', text1, text2, onPress}) on the host's toast. Fieldr's entry (titles can contain HTML, so it converts to text):

tsx
1notification: ({text1, text2, props, onPress}: any) => (2  <TouchableOpacity activeOpacity={0.8} disabled={!onPress} onPress={onPress}>3    <Text>{htmlToText(text1 || props?.title)}</Text>4    <Text>{htmlToText(text2 || props?.body)}</Text>5  </TouchableOpacity>6),

Fieldr sets foregroundSystemNotifications: true, so foreground notifications usually show as Notifee system notifications; the toast entry is the fallback.

Fieldr native files

FileFieldr content
ios/Podfilesetup_permissions(['Camera', 'PhotoLibrary', 'PhotoLibraryAddOnly', 'LocationWhenInUse', 'LocationAlways', 'MediaLibrary', 'Microphone', 'Motion']), $RNFirebaseAsStaticFramework = true, target 'FieldrBroadcast' do inherit! :search_paths; pod 'RealtimexLiveChatBroadcast', :path => '../node_modules/@realtimexsco/live-chat-react-native' end
ios/Fieldr/Info.plistRTCAppGroupIdentifier, RTCScreenSharingExtension, background modes audio, voip, location, remote-notification. Add picture-in-picture for call PiP on iOS
ios/Fieldr/AppDelegate.swiftFirebaseApp.configure()
android/build.gradle, android/app/build.gradleGoogle Services classpath 4.4.1 + plugin
android/app/src/main/AndroidManifest.xmlsupportsPictureInPicture on MainActivity
android/app/src/main/java/com/softwareco/fieldr/MainApplication.ktNo chat code — a comment notes CallPipModule comes from autolinking and must not be re-added
PreviousAll exportsNextHost app integration — providers, toasts, tab bars & navigation

On this page

package.json — dependencymetro.config.js — local packagesrc/navigation/rootNavigation.tsx — call route + session hostsrc/screens/chat/ChatSessionHost.tsx — app-wide sessionsrc/screens/chat/ChatScreen.tsx — the Chat tabsrc/screens/chat/scoped/useScopedChatConfig.ts — same config without stylessrc/screens/chat/scoped/ScopedChatRoomScreen.tsx — one room from another screensrc/utils/GlobalNotificationListener.tsx — host push listenersrc/utils/pushDevice.ts — logoutsrc/utils/toastConfig.tsx — in-app bannerFieldr native files
Back to top