Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: tweaks for ringing docs #692

Merged
merged 4 commits into from
May 31, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 174 additions & 126 deletions docusaurus/docs/Flutter/05-advanced/02-ringing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,176 @@
To get the best experience we strongly suggest using APNs for iOS. While our goal is to ensure compatibility with both providers on iOS, Firebase is not yet fully supported.
:::

### Creating a ringing Call

To create a ringing call, you need to follow the same steps as in the basic call flow with the difference of adding the `ringing` and `memberIds` paramteres to `getOrCreateCall` method.

Check failure on line 29 in docusaurus/docs/Flutter/05-advanced/02-ringing.mdx

View workflow job for this annotation

GitHub Actions / vale

[vale] docusaurus/docs/Flutter/05-advanced/02-ringing.mdx#L29

[Vale.Spelling] Did you really mean 'paramteres'?
Raw output
{"message": "[Vale.Spelling] Did you really mean 'paramteres'?", "location": {"path": "docusaurus/docs/Flutter/05-advanced/02-ringing.mdx", "range": {"start": {"line": 29, "column": 147}}}, "severity": "ERROR"}
Brazol marked this conversation as resolved.
Show resolved Hide resolved

```dart
final call = StreamVideo.instance.makeCall(callType: StreamCallType(), id: 'Your-call-ID');
await call.getOrCreate(memberIds: ['user1_id', 'user2_id'], ringing: true);
```

`ringing` set to true, will make Stream send a notification to the users on the call, triggering the platform call screen on iOS and Android.
Brazol marked this conversation as resolved.
Show resolved Hide resolved
`memberIds` is a list of user IDs we would like to immediately add to the call, combined with a `ringing` parameter will trigger the call to ring on the other side.
Brazol marked this conversation as resolved.
Show resolved Hide resolved

In subsequent steps, we will show you how to configure your Stream app in the Dashboard and your Flutter app to properly send and receive ringing notifications.

### Common steps for both iOS and Android

#### Configuring Push Notification Manager

To handle push notifications, you need to configure the `pushNotificationManagerProvider` in the `StreamVideo` instance.
It manages device token registration, incoming call handling and listening to call events (for example to end call on collee side when caller ends the call).

Check failure on line 46 in docusaurus/docs/Flutter/05-advanced/02-ringing.mdx

View workflow job for this annotation

GitHub Actions / vale

[vale] docusaurus/docs/Flutter/05-advanced/02-ringing.mdx#L46

[Vale.Spelling] Did you really mean 'collee'?
Raw output
{"message": "[Vale.Spelling] Did you really mean 'collee'?", "location": {"path": "docusaurus/docs/Flutter/05-advanced/02-ringing.mdx", "range": {"start": {"line": 46, "column": 119}}}, "severity": "ERROR"}
Brazol marked this conversation as resolved.
Show resolved Hide resolved

When creating a `StreamVideo` instance, you need to pass a `pushNotificationManagerProvider` parameter. This parameter is an instance of `StreamVideoPushNotificationManager` that is created using the `StreamVideoPushNotificationManager.create` method.

```dart
StreamVideo(
// ...
// Make sure you initialise push notification manager
pushNotificationManagerProvider: StreamVideoPushNotificationManager.create(
iosPushProvider: const StreamVideoPushProvider.apn(
name: 'your-ios-provider-name',
),
androidPushProvider: const StreamVideoPushProvider.firebase(
name: 'your-fcm-provider',
),
pushParams: const StreamVideoPushParams(
appName: kAppName,
ios: IOSParams(iconName: 'IconMask'),
),
),
);
```

- For `androidPushProvider` use the provider name we will create later in [Firebase integration](#Step-2---Upload-the-Firebase-Credentials-to-Stream)

- For `iosPushProvider` use the provider name we will create later in [APN integration](#Integrating-APNs-for-iOS​)

- Add app icon Asset in Xcode for displaying in CallKit screen dedicated button (named `IconMask` in the code below). [See details here](https://developer.apple.com/documentation/callkit/cxproviderconfiguration/2274376-icontemplateimagedata)

#### Handling CallKit events (for both iOS and Android)

CallKit events are events exposed by `flutter_callkit_incoming` package that we utilize to handle incoming calls on both iOS and Android.
It's important to handle these events to ensure a seamless calling experience regardles of which provider is used for push.

Check failure on line 78 in docusaurus/docs/Flutter/05-advanced/02-ringing.mdx

View workflow job for this annotation

GitHub Actions / vale

[vale] docusaurus/docs/Flutter/05-advanced/02-ringing.mdx#L78

[Vale.Spelling] Did you really mean 'regardles'?
Raw output
{"message": "[Vale.Spelling] Did you really mean 'regardles'?", "location": {"path": "docusaurus/docs/Flutter/05-advanced/02-ringing.mdx", "range": {"start": {"line": 78, "column": 79}}}, "severity": "ERROR"}

In a high-level widget in your app, add this code to listen to CallKit events:
Brazol marked this conversation as resolved.
Show resolved Hide resolved

```dart
final _callKitEventSubscriptions = Subscriptions();

@override
void initState() {
...
_observeCallKitEvents()
}

void _observeCallKitEvents() {
final streamVideo = StreamVideo.instance;
_callKitEventSubscriptions.addAll([
streamVideo.onCallKitEvent<ActionCallAccept>(_onCallAccept),
streamVideo.onCallKitEvent<ActionCallDecline>(_onCallDecline),
streamVideo.onCallKitEvent<ActionCallEnded>(_onCallEnded),
// You can add additional events if you need to handle them
// As an example, handling mute events from CallKit
]);
}

void _onCallAccept(ActionCallAccept event) async {
final streamVideo = StreamVideo.instance;

final uuid = event.data.uuid;
final cid = event.data.callCid;
if (uuid == null || cid == null) return;

final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid);
final callToJoin = call.getDataOrNull();
if (callToJoin == null) return;

var acceptResult = await callToJoin.accept();

// Return if cannot accept call
if(acceptResult.isFailure) {
debugPrint('Error accepting call: $call');
return;
}

// Data for next screen
final extra = (
call: callToJoin,
connectOptions: const CallConnectOptions(),
);

// Navigate to next screen if needed
}

void _onCallDecline(ActionCallDecline event) async {
final streamVideo = StreamVideo.instance;

final uuid = event.data.uuid;
final cid = event.data.callCid;
if (uuid == null || cid == null) return;

final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid);
final callToReject = call.getDataOrNull();
if (callToReject == null) return;

final result = await callToReject.reject();
if (result is Failure) {
debugPrint('Error rejecting call: ${result.error}');
}
}

void _onCallEnded(ActionCallEnded event) async {
final streamVideo = StreamVideo.instance;

final uuid = event.data.uuid;
final cid = event.data.callCid;
if (uuid == null || cid == null) return;

final call = streamVideo.activeCall;
if (call == null || call.callCid.value != cid) return;

final result = await call.leave();
if (result is Failure) {
debugPrint('Error leaving call: ${result.error}');
}
}

@override
void dispose() {
// ...
_callKitEventSubscriptions.cancelAll();
}
```

:::note
Remember to implement navigation in a marked line.
:::

Please add an additional extension at the end of the class / in a different file that we added to make
subscriptions simpler:

```dart
extension on Subscriptions {
void addAll<T>(Iterable<StreamSubscription<T>?> subscriptions) {
for (var i = 0; i < subscriptions.length; i++) {
final subscription = subscriptions.elementAt(i);
if (subscription == null) continue;

add(i + 100, subscription);
}
}
}
```

If you need to manage the CallKit call, you can use the `StreamVideo.pushNotificationManager`. As an example, let's
say you want to end all calls on the CallKit side, you can end them this way:

```dart
StreamVideo.instance.pushNotificationManager?.endAllCalls();
```

### Integrating Firebase for Android

#### Step 1 - Get the Firebase Credentials
Expand Down Expand Up @@ -97,11 +267,7 @@

- Replace `yourUserCredentialsGetMethod()` with your implementation to get logged in user credentials

- For `androidPushProvider` use the provider name created in [Step 2](#Step-2---Upload-the-Firebase-Credentials-to-Stream)

- For `iosPushProvider` use the provider name we will create later in [APN integration](#Integrating-APNs-for-iOS​)

- Add app icon Asset in Xcode for displaying in CallKit screen dedicated button (named `IconMask` in the code below). [See details here](https://developer.apple.com/documentation/callkit/cxproviderconfiguration/2274376-icontemplateimagedata)
- Configure `pushNotificationManagerProvider` in the same way you did in the previous setup steps

```dart
// As this runs in a separate isolate, we need to setup the app again.
Expand Down Expand Up @@ -219,6 +385,7 @@

Remember to follow [official best practices](https://developer.android.com/develop/ui/views/notifications/notification-permission#best-practices) (especially showing prompt before the request).


### Integrating APNs for iOS

#### Step 1 - Get the iOS certificate for push notifications
Expand Down Expand Up @@ -282,126 +449,7 @@
</array>
```

#### Step 5 - Add code to handle CallKit events

In a high-level widget in your app, add this code to listen to CallKit events:

```dart
final _callKitEventSubscriptions = Subscriptions();

@override
void initState() {
...
_observeCallKitEvents()
}

void _observeCallKitEvents() {
final streamVideo = StreamVideo.instance;
_callKitEventSubscriptions.addAll([
streamVideo.onCallKitEvent<ActionCallAccept>(_onCallAccept),
streamVideo.onCallKitEvent<ActionCallDecline>(_onCallDecline),
streamVideo.onCallKitEvent<ActionCallEnded>(_onCallEnded),
// You can add additional events if you need to handle them
// As an example, handling mute events from CallKit
]);
}

void _onCallAccept(ActionCallAccept event) async {
final streamVideo = StreamVideo.instance;

final uuid = event.data.uuid;
final cid = event.data.callCid;
if (uuid == null || cid == null) return;

final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid);
final callToJoin = call.getDataOrNull();
if (callToJoin == null) return;

var acceptResult = await callToJoin.accept();

// Return if cannot accept call
if(acceptResult.isFailure) {
debugPrint('Error accepting call: $call');
return;
}

// Data for next screen
final extra = (
call: callToJoin,
connectOptions: const CallConnectOptions(),
);

// Navigate to next screen if needed
}

void _onCallDecline(ActionCallDecline event) async {
final streamVideo = StreamVideo.instance;

final uuid = event.data.uuid;
final cid = event.data.callCid;
if (uuid == null || cid == null) return;

final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid);
final callToReject = call.getDataOrNull();
if (callToReject == null) return;

final result = await callToReject.reject();
if (result is Failure) {
debugPrint('Error rejecting call: ${result.error}');
}
}

void _onCallEnded(ActionCallEnded event) async {
final streamVideo = StreamVideo.instance;

final uuid = event.data.uuid;
final cid = event.data.callCid;
if (uuid == null || cid == null) return;

final call = streamVideo.activeCall;
if (call == null || call.callCid.value != cid) return;

final result = await call.leave();
if (result is Failure) {
debugPrint('Error leaving call: ${result.error}');
}
}

@override
void dispose() {
// ...
_callKitEventSubscriptions.cancelAll();
}
```

:::note
Remember to implement navigation in a marked line.
:::

Please add an additional extension at the end of the class / in a different file that we added to make
subscriptions simpler:

```dart
extension on Subscriptions {
void addAll<T>(Iterable<StreamSubscription<T>?> subscriptions) {
for (var i = 0; i < subscriptions.length; i++) {
final subscription = subscriptions.elementAt(i);
if (subscription == null) continue;

add(i + 100, subscription);
}
}
}
```

If you need to manage the CallKit call, you can use the `StreamVideo.pushNotificationManager`. As an example, let's
say you want to end all calls on the CallKit side, you can end them this way:

```dart
StreamVideo.instance.pushNotificationManager?.endAllCalls();
```

#### Step 6 - Add callback to handle call in terminated state
#### Step 5 - Add callback to handle call in terminated state

When an iOS app is terminated, the Flutter engine is not running. The engine needs to be started up to handle Stream call events whenever a call is received by the app. The Stream SDK performs the job of running a Flutter engine instance whenever a call is received. However, on the app side, a callback handle needs to be registered that will connect to `StreamVideo`.

Expand Down Expand Up @@ -443,7 +491,7 @@
);
```

#### Step 7 - Add native code to the iOS project
#### Step 6 - Add native code to the iOS project

In your iOS project, add the following imports to your `AppDelegate.swift`:

Expand Down
Loading