diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index e49f8a7bf8..95fd1197e7 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -51,7 +51,7 @@ "react-native-teleport": "^1.1.12", "react-native-web": "^0.21.2", "react-native-worklets": "0.11.1", - "stream-chat": "^10.0.0-rc.16", + "stream-chat": "^10.0.0-rc.17", "stream-chat-expo": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index da1b72961d..57f72b897a 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -65,7 +65,7 @@ "react-native-teleport": "^1.1.12", "react-native-video": "^6.19.2", "react-native-worklets": "^0.12.1", - "stream-chat": "^10.0.0-rc.16", + "stream-chat": "^10.0.0-rc.17", "stream-chat-react-native": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/SampleApp/src/ChatUsers.ts b/examples/SampleApp/src/ChatUsers.ts index 6e76b6d673..042e50eef4 100644 --- a/examples/SampleApp/src/ChatUsers.ts +++ b/examples/SampleApp/src/ChatUsers.ts @@ -1,31 +1,12 @@ import { ClientUser } from 'stream-chat'; -export const USER_TOKENS: Record = { - e2etest1: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZTJldGVzdDEifQ.XlQOw8nl7fFzHoBkEiTcYGkNo5r7EBYA40LABGOk4hc', - e2etest2: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZTJldGVzdDIifQ.2ZsHCMJ7i0vZvRJ5yoT-bm8OD_KAzBgJ-kB6bHGZ4FI', - e2etest3: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZTJldGVzdDMifQ.RWHY-MYkpP8FTJkfgrxUlCQhwap6eB7DTsp_HsZ1oIw', - khushal: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoia2h1c2hhbCJ9.NG3b6I8MgkLevwuTTqDXTpOol-Yj_1NCyvxewL_tg4U', - neil: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibmVpbCJ9.ty2YhwFaVEYkq1iUfY8s1G0Um3MpiVYpWK-b5kMky0w', - qatest1: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.5Nnj6MsauhjP7_D8jW9WbRovLv5uaxn8LPZZ-HB3mh4', - qatest2: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.9v0igQXv_IfL_uKZ6Xz9MXJOS-DMdkeWScJv-POFoTE', - thierry: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.Rm6o4HHlUE5zYoaJG1YoEE-V219j4KHTqZ9zqXlCHBQ', - tommaso: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.sfDKQ5peCwo0oObPDo-AFKauqv5uYbesukOOLpKaDOQ', - vir: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlyIn0.Whk_WyeN9TIuNahSu3KFVRTohF7HDbQq9Ka17kZ6zq4', - vishal: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.HOlVh-ZyQnjyuL20G-67RTgKufBuAH-I-gbEELFlass', - ivan5: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiaXZhbjUifQ.c3nq6rlnqyNeCRcwiW0VQ413nHl99pF72Ia2V_N84yU', - rodolphe: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicm9kb2xwaGUifQ.tLl-I8ADBhTKB-x5FB9jK4-am0dELLXgydM6VN9rTL8', -}; +/** + * Optional static tokens, keyed by user id. A user listed here logs in with this token; any other + * user gets one from the token generator (see `utils/tokenProvider.ts`). Empty by default, e.g. + * `neil: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'`. + */ +export const USER_TOKENS: Partial> = {}; + /** * Login fixtures, not server responses: these are the payloads handed to `connectUser`, so they * carry only `id` / `name` / `image`. Annotated `ClientUser` (everything optional but `id`) rather diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index d6ebf32061..3abc2ad9da 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -14,6 +14,7 @@ import { SqliteClient } from 'stream-chat-react-native'; import { USER_TOKENS, USERS } from '../ChatUsers'; import type { LoginConfig } from '../types'; import AsyncStore from '../utils/AsyncStore'; +import { getTokenOrProvider, SAMPLE_APP_API_KEY } from '../utils/tokenProvider'; const messaging = getMessaging(); @@ -98,7 +99,7 @@ export const useChatClient = () => { image: config.userImage, name: config.userName, }; - await client.connectUser(user, config.userToken); + await client.connectUser(user, getTokenOrProvider(config)); await AsyncStore.setItem('@stream-rn-sampleapp-login-config', config); const permissionAuthStatus = await messaging.hasPermission(); @@ -175,7 +176,7 @@ export const useChatClient = () => { try { if (userId) { await loginUser({ - apiKey: 'yjrt5yxw77ev', + apiKey: SAMPLE_APP_API_KEY, userId: USERS[userId].id, userImage: USERS[userId].image, userName: USERS[userId].name, diff --git a/examples/SampleApp/src/screens/AdvancedUserSelectorScreen.tsx b/examples/SampleApp/src/screens/AdvancedUserSelectorScreen.tsx index 2a048c6e4b..d74b84fbef 100644 --- a/examples/SampleApp/src/screens/AdvancedUserSelectorScreen.tsx +++ b/examples/SampleApp/src/screens/AdvancedUserSelectorScreen.tsx @@ -6,6 +6,7 @@ import { KeyboardCompatibleView, useTheme, version } from 'stream-chat-react-nat import { useAppContext } from '../context/AppContext'; import { useLegacyColors } from '../theme/useLegacyColors'; +import { DEFAULT_TOKEN_URL } from '../utils/tokenProvider'; const styles = StyleSheet.create({ safeArea: { flex: 1 }, @@ -147,7 +148,8 @@ export const AdvancedUserSelectorScreen: React.FC = () => { const [userIdError, setUserIdError] = useState(false); const [userName, setUserName] = useState(''); const [userToken, setUserToken] = useState(''); - const [userTokenError, setUserTokenError] = useState(false); + const [tokenUrl, setTokenUrl] = useState(DEFAULT_TOKEN_URL); + const [tokenError, setTokenError] = useState(false); const isValidInput = () => { let isValid = true; @@ -161,8 +163,9 @@ export const AdvancedUserSelectorScreen: React.FC = () => { isValid = false; } - if (!userToken) { - setUserTokenError(true); + // Either credential will do: a static token wins, otherwise one is fetched from the token URL. + if (!userToken && !tokenUrl) { + setTokenError(true); isValid = false; } @@ -201,14 +204,23 @@ export const AdvancedUserSelectorScreen: React.FC = () => { value={userId} /> { - setUserTokenError(false); + setTokenError(false); setUserToken(text); }} value={userToken} /> + { + setTokenError(false); + setTokenUrl(text); + }} + value={tokenUrl} + /> { @@ -228,8 +240,9 @@ export const AdvancedUserSelectorScreen: React.FC = () => { await loginUser({ apiKey, userId, + tokenUrl: tokenUrl || undefined, userName, - userToken, + userToken: userToken || undefined, }); } catch (e) { Alert.alert( diff --git a/examples/SampleApp/src/types.ts b/examples/SampleApp/src/types.ts index 1fd197bdd9..aa300c0796 100644 --- a/examples/SampleApp/src/types.ts +++ b/examples/SampleApp/src/types.ts @@ -74,7 +74,10 @@ export type AppTheme = Theme & { export type LoginConfig = { apiKey: string; userId: string; - userToken: string; + /** Static token. When omitted, tokens are fetched from `tokenUrl`. */ + userToken?: string; + /** Token generator URL; `user_id` is appended. Defaults to `DEFAULT_TOKEN_URL`. */ + tokenUrl?: string; userImage?: string; userName?: string; }; diff --git a/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts b/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts index b503629159..114ab88f7c 100644 --- a/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts +++ b/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts @@ -8,6 +8,8 @@ import { DeliveredMessagePayload, StreamChat } from 'stream-chat'; import AsyncStore from './AsyncStore'; +import { getTokenOrProvider } from './tokenProvider'; + import { LoginConfig } from '../types'; const messaging = getMessaging(); @@ -47,7 +49,7 @@ setBackgroundMessageHandler(messaging, async (remoteMessage) => { } const chatClient = StreamChat.getInstance(loginConfig.apiKey); // eslint-disable-next-line no-underscore-dangle -- stream-chat exposes _setToken as a public-but-underscore-prefixed method for background contexts - await chatClient._setToken({ id: loginConfig.userId }, loginConfig.userToken); + await chatClient._setToken({ id: loginConfig.userId }, getTokenOrProvider(loginConfig)); const notification = remoteMessage.data; diff --git a/examples/SampleApp/src/utils/tokenProvider.ts b/examples/SampleApp/src/utils/tokenProvider.ts new file mode 100644 index 0000000000..329dbc93b7 --- /dev/null +++ b/examples/SampleApp/src/utils/tokenProvider.ts @@ -0,0 +1,39 @@ +import type { TokenOrProvider, TokenProvider } from 'stream-chat'; + +import type { LoginConfig } from '../types'; + +export const SAMPLE_APP_API_KEY = 'yjrt5yxw77ev'; + +/** + * Pronto's token generator — the same one stream-chat-react's vite example uses — scoped to the + * SampleApp's app via `environment`. It signs tokens for {@link SAMPLE_APP_API_KEY} only; a custom + * app needs its own generator URL or a static token. + */ +export const DEFAULT_TOKEN_URL = + 'https://pronto.getstream.io/api/auth/create-token?environment=react-native-chat-sample'; + +/** + * Appends `user_id` by hand rather than through `URL.searchParams`: React Native's `URL` polyfill + * builds a fresh `URLSearchParams` on every `searchParams` read, so a `set()` on it never reaches + * `toString()`. + */ +const buildTokenRequestUrl = (tokenUrl: string, userId: string) => + `${tokenUrl}${tokenUrl.includes('?') ? '&' : '?'}user_id=${encodeURIComponent(userId)}`; + +export const createTokenProvider = + (tokenUrl: string, userId: string): TokenProvider => + async () => { + const response = await fetch(buildTokenRequestUrl(tokenUrl, userId)); + if (!response.ok) { + throw new Error(`Token generator responded with ${response.status}`); + } + const { token } = (await response.json()) as { token?: string }; + if (!token) { + throw new Error('Token generator response did not contain a token'); + } + return token; + }; + +/** A static token wins when one is provided; otherwise tokens come from the generator. */ +export const getTokenOrProvider = ({ tokenUrl, userId, userToken }: LoginConfig): TokenOrProvider => + userToken || createTokenProvider(tokenUrl || DEFAULT_TOKEN_URL, userId); diff --git a/package/package.json b/package/package.json index 57850475cd..34de6a9e73 100644 --- a/package/package.json +++ b/package/package.json @@ -80,7 +80,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^2.0.0", - "stream-chat": "^10.0.0-rc.16", + "stream-chat": "^10.0.0-rc.17", "use-sync-external-store": "^1.7.0" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index ec4ca8557e..2309364b01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6610,7 +6610,7 @@ __metadata: react-native-teleport: "npm:^1.1.12" react-native-web: "npm:^0.21.2" react-native-worklets: "npm:0.11.1" - stream-chat: "npm:^10.0.0-rc.16" + stream-chat: "npm:^10.0.0-rc.17" stream-chat-expo: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:6.0.3" @@ -17608,7 +17608,7 @@ __metadata: react-native-teleport: "npm:^1.1.12" react-native-video: "npm:^6.19.2" react-native-worklets: "npm:^0.12.1" - stream-chat: "npm:^10.0.0-rc.16" + stream-chat: "npm:^10.0.0-rc.17" stream-chat-react-native: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:6.0.3" @@ -18352,7 +18352,7 @@ __metadata: react-native-worklets: "npm:^0.12.1" react-test-renderer: "npm:19.2.3" rimraf: "npm:^6.0.1" - stream-chat: "npm:^10.0.0-rc.16" + stream-chat: "npm:^10.0.0-rc.17" typescript: "npm:6.0.3" use-sync-external-store: "npm:^1.7.0" uuid: "npm:^11.1.0" @@ -18426,9 +18426,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:^10.0.0-rc.16": - version: 10.0.0-rc.16 - resolution: "stream-chat@npm:10.0.0-rc.16" +"stream-chat@npm:^10.0.0-rc.17": + version: 10.0.0-rc.17 + resolution: "stream-chat@npm:10.0.0-rc.17" dependencies: "@stream-io/logger": "npm:^2.0.0" "@stream-io/state-store": "npm:^1.1.6" @@ -18439,7 +18439,7 @@ __metadata: built: true husky: built: true - checksum: 10c0/accb88f0763ff513f9a3899c4a96c32968067056898232ee5ad58540482ee0794fc06d13862904770d2d9fb65907b54b767a57f3f26ce613ef71e22228e7a3a2 + checksum: 10c0/f4721fb4db398ff465bf58a918aa4cd4a46085cc57ce305019c65fdda5b710dfaa14af9870bf060894ab35053fe4697e6ca29276323a625bf8b445c9ca274b77 languageName: node linkType: hard