Skip to content

Commit

Permalink
feat(react-native): add expo video sample app (#974)
Browse files Browse the repository at this point in the history
This PR focuses on adding expo video sample app for React Native SDK

---------

Co-authored-by: Santhosh Vaiyapuri <[email protected]>
Co-authored-by: Santhosh Vaiyapuri <[email protected]>
Co-authored-by: Vishal Narkhede <[email protected]>
  • Loading branch information
4 people authored Sep 7, 2023
1 parent ae25639 commit 3c61756
Show file tree
Hide file tree
Showing 134 changed files with 7,502 additions and 93 deletions.
1 change: 1 addition & 0 deletions packages/react-native-sdk/app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./expo-config-plugin/build');
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
title: Installation
description: Install the SDK for video calling
title: React Native
---

Installation and usage of our React Native SDK is simple and involves the following steps:
Expand Down Expand Up @@ -38,13 +37,14 @@ So what did we install precisely?

### Add Stream Video SDK's setup method

Once the peer dependencies are installed, a native setup method is needed to be added:
Once the peer dependencies are installed, a native setup method needs to be added:

#### Android

Add the following in your `MainApplication.java` file:

<!-- vale off -->

```java
// highlight-next-line
import com.streamvideo.reactnative.StreamVideoReactNative;
Expand All @@ -60,6 +60,7 @@ public class MainApplication extends Application implements ReactApplication {
}
}
```

<!-- vale on -->

#### iOS
Expand Down Expand Up @@ -153,9 +154,9 @@ If you plan to also support Bluetooth devices then also add the following.
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```


:::note
The SDK needs to know to the state of camera and microphone permissions to get video and audio streams. We can inform the states to the SDK through the following:

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

Expand All @@ -164,7 +165,8 @@ StreamVideoRN.setPermissions({
isMicPermissionGranted: true,
});
```
We have discussed a detailed solution to manage native runtime permissions in the [Manage Native Permissions](../../core/native-permissions) guide.

We have discussed a detailed solution to manage native runtime permissions in the [Manage Native Permissions](../../core/native-permissions) guide.
:::

### Optional peer dependencies
Expand All @@ -181,4 +183,3 @@ To enable this feature you need to install the following dependencies:
- `@react-native-voip-push-notification` to receive notifications on iOS.

More about how to enable this feature can be found [in our push notification guide](../../advanced/push-notifications/setup).

Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Expo
description: Install the SDK in Expo Development Builds
---

Our SDK is not available on Expo Go due to native code being required, but you can use the [expo-dev-client](https://docs.expo.dev/development/create-development-builds/) library to run your Expo app with a development build.

## Development Build

If you haven't already, prepare your project for [expo development builds](https://docs.expo.dev/develop/development-builds/installation/).

## SDK Installation

Add the SDK and its required dependencies to your project:

```bash title=Terminal
npx expo install @stream-io/video-react-native-sdk
npx expo install @stream-io/react-native-webrtc
npx expo install @config-plugins/react-native-webrtc
npx expo install react-native-incall-manager
npx expo install react-native-svg
npx expo install @react-native-community/netinfo
npx expo install @notifee/react-native
```

So what did we install precisely?

- `@stream-io/video-react-native-sdk` (SVRN) is Stream's Video SDK which contains UI components, hooks and util functions that will enable audio/video calls.
- `@stream-io/react-native-webrtc` is a WebRTC module for React Native, SVRN depends on this dependency, it's components and utilities to render audio/video tracks and interact with the phone's media devices.
- `react-native-incall-manager` handles media-routes/sensors/events during an audio/video call.
- `react-native-svg` provides SVG support to React Native, SVRN's components and it's icons are reliant on this dependency.
- `@react-native-community/netinfo` - is used to detect the device's connectivity state, type and quality.
- `@notifee/react-native` - is used to keep calls alive in the background on Android.

## Add config plugin

Add the config plugin for [`@stream-io/video-react-native-sdk`](https://github.com/GetStream/stream-video-js/tree/main/packages/react-native-sdk/expo-config-plugin/README.md) and [`react-native-webrtc`](https://www.npmjs.com/package/@config-plugins/react-native-webrtc) to your `app.json` file:

```js title=app.json
{
"expo": {
...
"plugins": [
// highlight-start
"@stream-io/video-react-native-sdk",
[
"@config-plugins/react-native-webrtc",
{
// optionally you can add your own explanations for permissions on iOS
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone"
}
]
// highlight-end
]
}
}
```

:::note
The `POST_NOTIFICATIONS`, `BLUETOOTH_CONNECT`, `BLUETOOTH` and `BLUETOOTH_ADMIN` permissions need to be requested and granted by the user as well. [PermissionsAndroid](https://reactnative.dev/docs/permissionsandroid) module can be used to request permissions in Android. For example, below is a way to request Bluetooth permissions in Android according to the OS version:

```js
import { useEffect } from 'react';
import { PermissionsAndroid, Platform } from 'react-native';

useEffect(() => {
const run = async () => {
if (Platform.OS === 'android' && Platform.Version <= 30) {
// highlight-start
await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.BLUETOOTH,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADMIN,
]);
} else {
await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
);
// highlight-end
}
};
run();
}, []);
```

:::
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label": "Installation"
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ The way to keep audio alive in the background is to enable the Voice over IP bac
```xml
<key>UIBackgroundModes</key>
<array>
<string>voip</string>
<string>audio</string>
</array>
```
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ Prop to customize the media controls in the Lobby component entirely.

Prop to customize the Join call button in the Lobby component.

| Type | Default Value |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ComponentType`\| `undefined` | [`LobbyControls`](https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/src/components/Call/Lobby/JoinCallButton.tsx) |
| Type | Default Value |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ComponentType`\| `undefined` | [`JoinCallButton`](https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/src/components/Call/Lobby/JoinCallButton.tsx) |
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ To receive push notifications from Stream Video Server, you'll need to:

1. Configure your push notification provider on the [Stream Dashboard](https://dashboard.getstream.io).
2. Add the native setup for both iOS and Android.
3. Setup the push config for the SDK.
3. Setup the push config for the SDK.

:::note
Push notifications are not supported by the SDK for Expo yet. We are working on this and it will be supported soon.
:::
85 changes: 85 additions & 0 deletions packages/react-native-sdk/expo-config-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
This config plugin is built to auto configure the `@stream-io/video-react-native-sdk` with the native changes.

After installing the `@stream-io/video-react-native-sdk` you can simply add the plugin in the `app.json` or `app.config.js` of your project as:

```json
{
"expo": {
"plugins": ["@stream-io/video-react-native-sdk"]
}
}
```

Next you can run the code using `yarn run android` and `yarn run ios`.

## Changes

The plugin adds the following native changes to the code.

### Android

#### `MainApplication.java`

Adds the import and setup for StreamVideoReactNative in your `MainApplication.java` file:

Read more about it [here](https://getstream.io/video/docs/reactnative/setup/installation/react-native/#add-stream-video-sdks-setup-method).

```java
// Adds this
import com.streamvideo.reactnative.StreamVideoReactNative;

public class MainApplication extends Application implements ReactApplication {

@Override
public void onCreate() {
super.onCreate();
// Adds this
StreamVideoReactNative.setup();
// the rest..
}
}
```

#### `AndroidManifest.xml`

Add service named `app.notifee.core.ForegroundService`.

```xml
<service android:name="app.notifee.core.ForegroundService" android:stopWithTask="true" android:foregroundServiceType="microphone"/>
```

The `@stream-io/video-react-native-sdk` also adds the appropriate android permissions such as `POST_NOTIFICATIONS`, `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_MICROPHONE`, `BLUETOOTH`, `BLUETOOTH_ADMIN` and `BLUETOOTH_CONNECT` to the `AndroidManifest.xml`.

### iOS

#### `AppDelegate.mm`

Adds the import and setup for StreamVideoReactNative in your `AppDelegate.mm` file:

Read more about it [here](https://getstream.io/video/docs/reactnative/setup/installation/react-native/#add-stream-video-sdks-setup-method).

```c
// Adds this
#import "StreamVideoReactNative.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Adds this
[StreamVideoReactNative setup];

// the rest..
}
```

### `Info.plist`

Adds `audio` to the `UIBackgroundModes` in Info.plist as:

```xml
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import withStreamVideoReactNativeSDKManifest from '../src/withAndroidManifest';
import { ExpoConfig } from '@expo/config-types';
import { AndroidConfig } from '@expo/config-plugins';
import { getFixturePath } from '../fixtures';

// Define a custom type that extends ExpoConfig
interface CustomExpoConfig extends ExpoConfig {
modResults: AndroidConfig.Manifest.AndroidManifest;
}

// the real withAndroidManifest doesnt return the updated config
// so we mock it to return the updated config using the callback we pass in the actual implementation
jest.mock('@expo/config-plugins', () => {
const originalModule = jest.requireActual('@expo/config-plugins');
return {
...originalModule,
withAndroidManifest: jest.fn((config, callback) => {
const updatedConfig: CustomExpoConfig = callback(
config as CustomExpoConfig,
);
return updatedConfig;
}),
};
});

const readAndroidManifestAsync =
AndroidConfig.Manifest.readAndroidManifestAsync;

const getMainApplicationOrThrow =
AndroidConfig.Manifest.getMainApplicationOrThrow;

const sampleManifestPath = getFixturePath('AndroidManifest.xml');

describe('withStreamVideoReactNativeSDKManifest', () => {
let modifiedConfig: CustomExpoConfig | undefined;
it('should modify Android Manifest', async () => {
const manifest = await readAndroidManifestAsync(sampleManifestPath);
// Prepare a mock config
const config: CustomExpoConfig = {
name: 'test-app',
slug: 'test-app',
modResults: manifest,
};

const updatedConfig = withStreamVideoReactNativeSDKManifest(
config,
) as CustomExpoConfig;

const mainApp = getMainApplicationOrThrow(updatedConfig.modResults);

expect(
mainApp.service?.some(
(service) =>
service.$['android:name'] === 'app.notifee.core.ForegroundService',
),
).toBe(true);

modifiedConfig = updatedConfig;
});

it('should not create duplicates', () => {
expect(modifiedConfig?.modResults).toBeDefined();

const updatedConfig = withStreamVideoReactNativeSDKManifest(
modifiedConfig!,
) as CustomExpoConfig;

const mainApp = getMainApplicationOrThrow(updatedConfig.modResults);

expect(
mainApp.service?.filter(
(service) =>
service.$['android:name'] === 'app.notifee.core.ForegroundService',
).length,
).toBe(1);

modifiedConfig = updatedConfig;
});

it('should throw error for malformed manifest', () => {
// Prepare a mock config
const config: CustomExpoConfig = {
name: 'test-app',
slug: 'test-app',
modResults: {
// @ts-expect-error: we are testing malformed manifest
bla: 'blabla',
},
};
expect(() => withStreamVideoReactNativeSDKManifest(config)).toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import withStreamVideoReactNativeSDKAndroidPermissions from '../src/withAndroidPermissions';
import { ExpoConfig } from '@expo/config-types';

describe('withStreamVideoReactNativeSDKAndroidPermissions', () => {
it('should add specified permissions to Android config', () => {
const inputConfig: ExpoConfig = {
// Your initial configuration here
name: 'test-app',
slug: 'test-app',
android: {
permissions: [],
},
};

const updatedConfig =
withStreamVideoReactNativeSDKAndroidPermissions(inputConfig);

// Assert that the necessary permissions are added to the Android config
expect(updatedConfig?.android?.permissions).toEqual(
expect.arrayContaining([
'android.permission.POST_NOTIFICATIONS',
'android.permission.FOREGROUND_SERVICE',
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
'android.permission.BLUETOOTH',
'android.permission.BLUETOOTH_CONNECT',
'android.permission.BLUETOOTH_ADMIN',
]),
);
});
});
Loading

0 comments on commit 3c61756

Please sign in to comment.