Skip to content

Commit

Permalink
feat(client): add a instance getter (#1461)
Browse files Browse the repository at this point in the history
* Created `StreamVideoClient.getOrCreateInstance` method
* Updated RN samples
* Tested on RN dogfood app, saves 3 unnecessary call.get() on incoming
call when app is in background
* Updated docs: ~React~, RN, ~JS~
  • Loading branch information
santhoshvai authored Aug 21, 2024
1 parent c09e648 commit 7f4d836
Show file tree
Hide file tree
Showing 16 changed files with 132 additions and 70 deletions.
49 changes: 49 additions & 0 deletions packages/client/src/StreamVideoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export class StreamVideoClient {
protected connectionPromise: Promise<void | ConnectedEvent> | undefined;
protected disconnectionPromise: Promise<void> | undefined;

private static _instanceMap: Map<string, StreamVideoClient> = new Map();

/**
* You should create only one instance of `StreamVideoClient`.
*/
Expand Down Expand Up @@ -120,13 +122,56 @@ export class StreamVideoClient {
const user = apiKeyOrArgs.user;
const token = apiKeyOrArgs.token || apiKeyOrArgs.tokenProvider;
if (user) {
let id = user.id;
if (user.type === 'anonymous') {
id = '!anon';
}
if (id) {
if (StreamVideoClient._instanceMap.has(apiKeyOrArgs.apiKey + id)) {
this.logger(
'warn',
`A StreamVideoClient already exists for ${user.type === 'anonymous' ? 'an anyonymous user' : id}; Prefer using getOrCreateInstance method`,
);
}
user.id = id;
StreamVideoClient._instanceMap.set(apiKeyOrArgs.apiKey + id, this);
}
this.connectUser(user, token).catch((err) => {
this.logger('error', 'Failed to connect', err);
});
}
}
}

public static getOrCreateInstance(args: {
apiKey: string;
user: User;
token?: string;
tokenProvider?: TokenProvider;
options?: StreamClientOptions;
}): StreamVideoClient {
const user = args.user;
if (!user.id) {
if (args.user.type === 'anonymous') {
user.id = '!anon';
} else {
throw new Error('User ID is required for a non-anonymous user');
}
}
if (!args.token && !args.tokenProvider) {
if (args.user.type !== 'anonymous' && args.user.type !== 'guest') {
throw new Error(
'TokenProvider or token is required for a user that is not a guest or anonymous',
);
}
}
let instance = StreamVideoClient._instanceMap.get(args.apiKey + user.id);
if (!instance) {
instance = new StreamVideoClient({ ...args, user });
}
return instance;
}

/**
* Return the reactive state store, use this if you want to be notified about changes to the client state
*/
Expand Down Expand Up @@ -268,6 +313,7 @@ export class StreamVideoClient {
if (!this.streamClient.user && !this.connectionPromise) {
return;
}
const userId = this.streamClient.user?.id;
const disconnectUser = () => this.streamClient.disconnectUser(timeout);
this.disconnectionPromise = this.connectionPromise
? this.connectionPromise.then(() => disconnectUser())
Expand All @@ -276,6 +322,9 @@ export class StreamVideoClient {
() => (this.disconnectionPromise = undefined),
);
await this.disconnectionPromise;
if (userId) {
StreamVideoClient._instanceMap.delete(userId);
}
this.eventHandlersToUnregister.forEach((unregister) => unregister());
this.eventHandlersToUnregister = [];
this.writeableStateStore.setConnectedUser(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class TokenManager {
}

throw new Error(
`Both secret and user tokens are not set. Either client.connectUser wasn't called or client.disconnect was called`,
`User token is not set. Either client.connectUser wasn't called or client.disconnect was called`,
);
};

Expand Down
4 changes: 0 additions & 4 deletions packages/client/src/devices/BrowserPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export class BrowserPermission {

this.ready = (async () => {
const assumeGranted = (error?: unknown) => {
this.logger('warn', "Can't query permissions, assuming granted", {
permission,
error,
});
this.setState('granted');
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const user: User = {
};
const apiKey = 'my-stream-api-key';
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const client = new StreamVideoClient({ apiKey, token, user });
const client = StreamVideoClient.getOrCreateInstance({ apiKey, token, user });
```

- The API Key can be found in your dashboard.
Expand All @@ -30,7 +30,7 @@ Typically, you'll want to initialize the client when your application loads and
## Generating a token

Tokens need to be generated server side. You can use our [server side SDKs](https://getstream.io/video/docs/api/authentication/) to quickly add support for this.
Typically, you integrate this logic into the part of your codebase where you log in or register users.
Typically, you integrate this into the part of your codebase where you log in or register users.
The tokens provide a way to authenticate a user or give access to a specific set of calls.

<TokenSnippet sampleApp="meeting" displayStyle="credentials" />
Expand All @@ -43,7 +43,7 @@ For development purposes, you can use our [Token Generator](https://getstream.io

- Authenticated users are users that have an account on your app.
- Guest users are temporary user accounts. You can use it to temporarily give someone a name and image when joining a call.
- Anonymous users are users that are not authenticated. It's common to use this type for watching a livestream or similar where you aren't authenticated.
- Anonymous users are users that are not authenticated. It's common to use this for watching a livestream or similar where you aren't authenticated.

This example shows the client setup for a guest user:

Expand All @@ -54,9 +54,8 @@ const user: User = {
id: 'jack-guest',
type: 'guest',
};

const apiKey = 'my-stream-api-key';
const client = new StreamVideoClient({ apiKey, user });
const client = StreamVideoClient.getOrCreateInstance({ apiKey, user });
```

And here's an example for an anonymous user
Expand All @@ -69,15 +68,18 @@ const user: User = {
};

const apiKey = 'my-stream-api-key';
const client = new StreamVideoClient({ apiKey, user });
const client = StreamVideoClient.getOrCreateInstance({ apiKey, user });
```

## Client options

### `token` or `tokenProvider`

To authenticate users you can either provide a string `token` or a `tokenProvider` function that returns `Promise<string>`.
If you use the `tokenProvider` the SDK will automatically call the provider whenever the token is expired.

:::info
If you use the `tokenProvider` the SDK will automatically execute it to refresh the token whenever the token is expired.
:::

```typescript
import { StreamVideoClient, User } from '@stream-io/video-react-native-sdk';
Expand All @@ -92,23 +94,25 @@ const user: User = {
id: 'sara',
};
const apiKey = 'my-stream-api-key';
const client = new StreamVideoClient({ apiKey, tokenProvider, user });
const client = StreamVideoClient.getOrCreateInstance({ apiKey, tokenProvider, user });
```

### Logging

You can configure the log level and the logger method used by the SDK.

The default log level is `warn`, other options are: `debug`, `info`, and `error`.
The default log level is `warn`, other options are: `trace`, `debug`, `info`, and `error`.

The default logger method will log to the browser `console`.
The default logger method will log to the debugging console.

```ts
import { StreamVideoClient, Logger } from '@stream-io/video-react-native-sdk';

const myLogger: Logger;
const myLogger: Logger = (logLevel, message, ...args) => {
// Do something with the log message
};

const client = new StreamVideoClient({
const client = StreamVideoClient.getOrCreateInstance({
apiKey,
token,
user,
Expand Down Expand Up @@ -138,7 +142,7 @@ export const MyApp = () => {
const [client, setClient] = useState<StreamVideoClient>();
useEffect(() => {
const tokenProvider = () => Promise.resolve('<token>');
const myClient = new StreamVideoClient({ apiKey, user, tokenProvider });
const myClient = StreamVideoClient.getOrCreateInstance({ apiKey, user, tokenProvider });
setClient(myClient);
return () => {
myClient.disconnectUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export function setPushConfig() {
// an example promise to fetch token from your server
const tokenProvider = () => yourServer.getTokenForUser(userId).then((auth) => auth.token);
const user = { id: userId, name: userName };
return new StreamVideoClient({
return StreamVideoClient.getOrCreateInstance({
apiKey: STREAM_API_KEY, // pass your stream api key
user,
tokenProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export function setPushConfig() {
// an example promise to fetch token from your server
const tokenProvider = () => yourServer.getTokenForUser(userId).then((auth) => auth.token);
const user = { id: userId, name: userName };
return new StreamVideoClient({
return StreamVideoClient.getOrCreateInstance({
apiKey: STREAM_API_KEY, // pass your stream api key
user,
tokenProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export function setPushConfig() {
// an example promise to fetch token from your server
const tokenProvider = () => yourServer.getTokenForUser(userId).then((auth) => auth.token);
const user = { id: userId, name: userName };
return new StreamVideoClient({
return StreamVideoClient.getOrCreateInstance({
apiKey: STREAM_API_KEY, // pass your stream api key
user,
tokenProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export function setPushConfig() {
// an example promise to fetch token from your server
const tokenProvider = () => yourServer.getTokenForUser(userId).then((auth) => auth.token);
const user = { id: userId, name: userName };
return new StreamVideoClient({
return StreamVideoClient.getOrCreateInstance({
apiKey: STREAM_API_KEY, // pass your stream api key
user,
tokenProvider,
Expand Down
17 changes: 9 additions & 8 deletions packages/react-native-sdk/src/utils/StreamVideoRN/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,15 @@ export type StreamVideoConfig = {
* createStreamVideoClient: async () => {
* const userId = await AsyncStorage.getItem('@userId');
* const userName = await AsyncStorage.getItem('@userName');
* const token = await AsyncStorage.getItem('@userToken');
* if (!username || !userToken) return undefined;
* const user = { id: userId, name: userName, token };
* return new StreamVideoClient({
* apiKey: STREAM_API_KEY,
* user,
* token,
* })
* const tokenProvider = async () => await AsyncStorage.getItem('@userToken');
* if (!username || !userId) return undefined;
* const user = { id: userId, name: userName };
* return StreamVideoClient.getOrCreateInstance({
* apiKey,
* tokenProvider,
* user
* });
* }
*/
createStreamVideoClient: () => Promise<StreamVideoClient | undefined>;
/** The callback that is called when a call is accepted, used for navigation */
Expand Down
12 changes: 6 additions & 6 deletions packages/react-native-sdk/src/utils/push/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ const firebaseMessagingOnMessageHandler = async (
},
});

if (asForegroundService) {
// no need to check if call has be closed as that will be handled by the fg service
return;
}

// check if call needs to be closed if accept/decline event was done
// before the notification was shown
const client = await pushConfig.createStreamVideoClient();
Expand All @@ -318,12 +323,7 @@ const firebaseMessagingOnMessageHandler = async (
const callFromPush = await client.onRingingCall(call_cid);

if (shouldCallBeClosed(callFromPush)) {
if (asForegroundService) {
notifee.stopForegroundService();
} else {
notifee.cancelDisplayedNotification(call_cid);
}
return;
notifee.cancelDisplayedNotification(call_cid);
}
} else {
const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ export const VideoWrapper = ({ children }: PropsWithChildren<{}>) => {
const { apiKey } = await fetchAuthDetails();
const tokenProvider = () => fetchAuthDetails().then((auth) => auth.token);
setState({ apiKey });
_videoClient = new StreamVideoClient({
_videoClient = StreamVideoClient.getOrCreateInstance({
apiKey,
user,
tokenProvider,
options: { logLevel: 'warn' },
});
setVideoClient(_videoClient);
};
run();
if (user.id) {
run();
}

return () => {
_videoClient?.disconnectUser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ export const useAnonymousInitVideoClient = () => {
const [client, setClient] = useState<StreamVideoClient>();

useEffect(() => {
let _client: StreamVideoClient | undefined;
const run = async () => {
_client = new StreamVideoClient({
apiKey,
user: { type: 'anonymous' },
});
setClient(_client);
};
run();
const _client = StreamVideoClient.getOrCreateInstance({
apiKey,
user: { type: 'anonymous' },
});

return () => {
_client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,22 @@ export const GuestMeetingScreen = (props: Props) => {
useEffect(() => {
let _videoClient: StreamVideoClient | undefined;
const run = async () => {
const { token, apiKey } = await createToken(
{
user_id: userToConnect.id ?? '!anon',
call_cids: mode === 'anonymous' ? `${callType}:${callId}` : undefined,
},
appEnvironment,
);
_videoClient = new StreamVideoClient({
const fetchAuthDetails = async () => {
return await createToken(
{
user_id: userToConnect.id ?? '!anon',
call_cids:
mode === 'anonymous' ? `${callType}:${callId}` : undefined,
},
appEnvironment,
);
};
const { apiKey } = await fetchAuthDetails();
const tokenProvider = () => fetchAuthDetails().then((auth) => auth.token);
_videoClient = StreamVideoClient.getOrCreateInstance({
apiKey,
user: userToConnect,
token,
tokenProvider,
options: { logLevel: 'warn' },
});
setVideoClient(_videoClient);
Expand Down
15 changes: 8 additions & 7 deletions sample-apps/react-native/dogfood/src/utils/setPushConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,16 @@ const createStreamVideoClient = async () => {
name: userName,
imageUrl: userImageUrl,
};
const { token, apiKey } = await createToken(
{ user_id: user.id },
appEnvironment,
);

const client = new StreamVideoClient({
const fetchAuthDetails = async () => {
return await createToken({ user_id: user.id }, appEnvironment);
};
const { apiKey } = await fetchAuthDetails();
const tokenProvider = () => fetchAuthDetails().then((auth) => auth.token);
const client = StreamVideoClient.getOrCreateInstance({
apiKey,
user,
token,
tokenProvider,
options: { logLevel: 'warn' },
});
return client;
};
Loading

0 comments on commit 7f4d836

Please sign in to comment.