[Problem/Question]
I’m implementing the basic logic for the UIKit into a new (within the last 6 months) expo app.
Following the guide and the sample react native app, I have created a layout which contains the sendbird wrapper, and uses the platformServices as outlined for expo in the documentation.
Within this is a stack navigator that houses the login, list fragment, channel create fragment and channel fragment.
So far, only using the app_id I have been able to sign in and find that currentUser updates correctly, I navigate to the GroupChannelListFragment, and receive an error ‘Invalid Parameters’. Should I comment out the jsx for GroupChannelListFragment (and still initialise it) the page will render without the error.
Below is the code and dependancy versions, any help is greatly appreciated. Thanks!
[Versions]
Core Sendbird Dependencies
Platform Services Dependencies
-
expo-av: ~15.1.7
-
expo-clipboard: ~7.1.5
-
expo-document-picker: ~13.1.6
-
expo-file-system: ~18.1.11
-
expo-image-manipulator: ~13.1.7
-
expo-image-picker: ~16.1.4
-
expo-media-library: ~17.1.7
-
expo-notifications: ^0.31.4
-
expo-video-thumbnails: ~9.1.3
Storage
- react-native-mmkv: ^3.3.3
React Native & Expo
-
react-native: 0.79.6
-
expo: ^53.0.11
-
expo-router: ~5.1.0
[Snippets]
_layout.tsx
import { SendbirdUIKitContainer, useSendbirdChat } from '@sendbird/uikit-react-native';
import { router, Stack, useRouter } from 'expo-router';
import React, { useEffect } from 'react';
import { MMKV } from 'react-native-mmkv';
import { APP_ID } from '../../env';
import platformServices, { setSendbirdSDK } from '../../util/sendbird';
export const mmkv = new MMKV();
export default function ChatLayout() {
return (
<SendbirdUIKitContainer
appId={APP_ID}
uikitOptions={{
common: {
enableUsingDefaultUserProfile: true,
},
groupChannel: {
enableMention: true,
// typingIndicatorTypes: new Set([TypingIndicatorType.Text, TypingIndicatorType.Bubble]),
// replyType: localConfigs.replyType,
// threadReplySelectType: localConfigs.threadReplySelectType,
},
groupChannelList: {
enableTypingIndicator: true,
enableMessageReceiptStatus: true,
},
groupChannelSettings: {
enableMessageSearch: true,
},
}}
chatOptions={{
localCacheStorage: mmkv,
onInitialized: (sdk) => setSendbirdSDK(sdk),
enableAutoPushTokenRegistration: false,
}}
platformServices={platformServices}
styles={{
defaultHeaderTitleAlign: 'left', //'center',
// theme: isLightTheme ? LightUIKitTheme : DarkUIKitTheme,
// statusBarTranslucent: GetTranslucent(),
}}
// errorBoundary={{ ErrorInfoComponent: ErrorInfoScreen }}
userProfile={{
onCreateChannel: (channel) => {
const params = { channelUrl: channel.url };
if (channel.isGroupChannel()) {
router.push({ pathname: '/(explore)/chat/channel', params });
}
if (channel.isOpenChannel()) {
router.push({ pathname: '/(explore)/chat/channel', params });
}
},
}}
>
<ChatNavigator />
</SendbirdUIKitContainer>
);
};
const ChatNavigator = () => {
const { currentUser } = useSendbirdChat();
const router = useRouter();
useEffect(() => {
console.log('[ChatLayout] currentUser changed:', currentUser?.userId);
if (currentUser) {
console.log('navigating to chatList');
router.replace('/(explore)/chat/chatList');
} else {
router.replace('/(explore)/chat/sign-in');
}
}, [currentUser, router]);
console.log('[ChatLayout] currentUser:', currentUser);
return (
<Stack screenOptions={{ headerShown: false }} initialRouteName="sign-in">
<Stack.Screen name="sign-in" />
<Stack.Screen name="chatList" />
<Stack.Screen name="create" />
<Stack.Screen name="channel" />
</Stack>
);
}
sendbird-utils.ts
import {
createExpoClipboardService,
createExpoFileService,
createExpoMediaService,
createExpoNotificationService,
createExpoPlayerService,
createExpoRecorderService,
SendbirdUIKitContainerProps
} from "@sendbird/uikit-react-native";
import { Logger, SendbirdChatSDK } from '@sendbird/uikit-utils';
import { APP_ID } from '../env';
import * as ExpoAV from 'expo-av';
import * as ExpoClipboard from 'expo-clipboard';
import * as ExpoDocumentPicker from 'expo-document-picker';
import * as ExpoFS from 'expo-file-system';
import * as ExpoImageManipulator from 'expo-image-manipulator';
import * as ExpoImagePicker from 'expo-image-picker';
import * as ExpoMediaLibrary from 'expo-media-library';
import * as ExpoNotifications from 'expo-notifications';
import * as ExpoVideoThumbnail from 'expo-video-thumbnails';
const platformServices: SendbirdUIKitContainerProps['platformServices'] = {
clipboard: createExpoClipboardService(ExpoClipboard),
notification: createExpoNotificationService(ExpoNotifications),
file: createExpoFileService({
fsModule: ExpoFS,
imagePickerModule: ExpoImagePicker,
mediaLibraryModule: ExpoMediaLibrary,
documentPickerModule: ExpoDocumentPicker,
}),
media: createExpoMediaService({
avModule: ExpoAV,
thumbnailModule: ExpoVideoThumbnail,
imageManipulator: ExpoImageManipulator,
fsModule: ExpoFS,
}),
player: createExpoPlayerService({
avModule: ExpoAV,
}),
recorder: createExpoRecorderService({
avModule: ExpoAV,
}),
};
export default platformServices;
let AppSendbirdSDK: SendbirdChatSDK | undefined;
export const getSendbirdSDK = () => AppSendbirdSDK;
export const setSendbirdSDK = (sdk: SendbirdChatSDK): SendbirdChatSDK => {
AppSendbirdSDK = sdk;
return sdk;
};
const createSendbirdAPI = (appId: string, apiToken: string) => {
const MIN = 60 * 1000;
const endpoint = (path: string) => `https://api-${appId}.sendbird.com/v3${path}`;
const getHeaders = (headers?: object) => ({ 'Api-Token': apiToken, ...headers });
return {
async getSessionToken(
userId: string,
expires_at = Date.now() + 10 * MIN,
): Promise<{ user_id: string; token: string; expires_at: number }> {
const res = await fetch(endpoint(`/users/${userId}/token`), {
method: 'post',
headers: getHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ expires_at }),
});
if (!res.ok) {
const text = await res.text();
Logger.warn('[SendbirdAPI] getSessionToken failed', res.status, text);
throw new Error(`Session token request failed: ${res.status}`);
}
return res.json();
},
};
};
export const SendbirdAPI = createSendbirdAPI(
APP_ID || '',
'API_TOKEN'
);
sign-in.tsx
import { SendbirdAPI } from '@/app/util/sendbird';
import { SessionHandler } from '@sendbird/chat';
import { useConnection, useSendbirdChat } from '@sendbird/uikit-react-native';
import { Button, Text, TextInput, useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
const SignInScreen = () => {
const [userId, setUserId] = useState('');
const [nickname, setNickname] = useState('');
const { sdk } = useSendbirdChat();
const { connect } = useConnection();
const connectWith = async (userId: string, nickname?: string, useSessionToken = false) => {
if (useSessionToken) {
try {
const sessionHandler = new SessionHandler();
sessionHandler.onSessionTokenRequired = (onSuccess, onFail) => {
SendbirdAPI.getSessionToken(userId)
.then(({ token }) => onSuccess(token))
.catch(onFail);
};
(sdk as any).setSessionHandler(sessionHandler);
const data = await SendbirdAPI.getSessionToken(userId);
await connect(userId, { nickname, accessToken: data.token });
} catch (e: any) {
console.error('[Sendbird] session connect failed', e?.code, e?.message || e);
throw e;
}
} else {
console.log('[Sendbird] connect start', { userId, nickname });
try {
await connect(userId, { nickname });
console.log('[Sendbird] connect success');
} catch (e: any) {
console.error('[Sendbird] connect failed', e?.code, e?.message || e);
}
}
};
const { colors } = useUIKitTheme();
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={styles.title}>{'Sendbird RN-UIKit sample'}</Text>
<TextInput
placeholder={'User ID'}
value={userId}
onChangeText={setUserId}
style={[styles.input, { backgroundColor: colors.onBackground04, marginBottom: 12 }]}
/>
<TextInput
placeholder={'Nickname'}
value={nickname}
onChangeText={setNickname}
style={[styles.input, { backgroundColor: colors.onBackground04 }]}
/>
<Button
style={styles.btn}
variant={'contained'}
onPress={async () => {
if (userId) {
await connectWith(userId, nickname);
}
}}
>
{'Sign in'}
</Button>
{/* <Versions style={{ marginTop: 12 }} /> */}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 80,
alignItems: 'center',
paddingHorizontal: 24,
},
logo: {
width: 48,
height: 48,
marginBottom: 24,
},
title: {
fontSize: 24,
fontWeight: '700',
marginBottom: 34,
},
btn: {
width: '100%',
paddingVertical: 16,
},
input: {
width: '100%',
borderRadius: 4,
marginBottom: 32,
paddingTop: 16,
paddingBottom: 16,
},
});
export default SignInScreen;
chatList.tsx
import { createGroupChannelListFragment, useSendbirdChat } from '@sendbird/uikit-react-native';
import { Stack, useRouter } from 'expo-router';
import React from 'react';
export default function ChatListScreen() {
const router = useRouter();
const { sdk } = useSendbirdChat();
console.log('sdk.connectionState', sdk.connectionState);
const GroupChannelListFragment = createGroupChannelListFragment();
console.log('GroupChannelListFragment', GroupChannelListFragment !== null);
return (
<>
<Stack.Screen
options={{
headerShown: true,
title: 'Chat',
}}
/>
<GroupChannelListFragment
onPressCreateChannel={(channelType) =>
router.push({ pathname: '/(explore)/chat/create', params: { channelType } })
}
onPressChannel={(channel) =>
router.push({ pathname: '/(explore)/chat/channel', params: { channelUrl: channel.url } })
}
/>
</>
);
}
[Outputs]
LOG [ChatLayout] currentUser: undefined
LOG [ChatLayout] currentUser changed: undefined
LOG [Sendbird] connect start {"nickname": "Tester", "userId": "Testing-user—login"}
LOG [Sendbird] connect success
LOG [ChatLayout] currentUser: {"_hashValue": 141110216, "_iid": "su-ce0095c9-e28b-4d2c-ab57-3afeb7460996", "_updatedAt": 0, "connectionStatus": "online", "friendDiscoveryKey": null, "friendName": null, "isActive": true, "lastSeenAt": 0, "metaData": {}, "nickname": "Tester", "plainProfileUrl": "", "preferredLanguages": [], "requireAuth": false, "userId": "Testing-user—login"}
LOG [ChatLayout] currentUser changed: Testing-user—login
LOG navigating to chatList
LOG sdk.connectionState OPEN
LOG GroupChannelListFragment true
ERROR Warning: SendbirdError: Invalid parameters.
This error is located at:
5 | export default function ChatListScreen() {
6 | const router = useRouter();
> 7 | const { sdk } = useSendbirdChat();
| ^
8 |
9 | console.log('sdk.connectionState', sdk.connectionState);
10 |
Call Stack
ChatListScreen (app/(explore)/chat/chatList.tsx:7:36)
ScreenContentWrapper (<anonymous>)
RNSScreenStack (<anonymous>)
ChatNavigator (app/(explore)/chat/_layout.tsx:69:42)
RNCSafeAreaProvider (<anonymous>)
ChatLayout(./(explore)/chat/_layout.tsx) (<anonymous>)
ScreenContentWrapper (<anonymous>)
RNSScreenStack (<anonymous>)
ThemedNavigation (app/_layout.tsx:71:48)
ThemeProvider (app/context/ThemeContext.tsx:883:82)
useEffect$argument_0 (app/_layout.tsx:51:24)
RootLayout (app/_layout.tsx:107:33)
RootApp(./_layout.tsx) (<anonymous>)
RNCSafeAreaProvider (<anonymous>)
App (<anonymous>)
ErrorOverlay (<anonymous>)