@fishjam-cloud/react-native-client 0.29.0 → 0.30.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/overrides/hooks.d.ts +3 -1
- package/dist/overrides/hooks.d.ts.map +1 -1
- package/dist/voip/VoIPContext.d.ts +99 -0
- package/dist/voip/VoIPContext.d.ts.map +1 -0
- package/dist/voip/VoIPContext.js +15 -0
- package/dist/voip/VoIPProvider.d.ts +26 -0
- package/dist/voip/VoIPProvider.d.ts.map +1 -0
- package/dist/voip/VoIPProvider.js +268 -0
- package/package.json +4 -4
- package/plugin/build/types.d.ts +21 -0
- package/plugin/build/withFishjamAndroid.js +5 -2
- package/plugin/build/withFishjamIos.js +56 -0
- package/plugin/build/withFishjamVoIPAndroid.d.ts +3 -0
- package/plugin/build/withFishjamVoIPAndroid.js +207 -0
package/README.md
CHANGED
|
@@ -10,6 +10,85 @@ npm install @fishjam-cloud/react-native-client
|
|
|
10
10
|
yarn add @fishjam-cloud/react-native-client
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Android VoIP setup
|
|
14
|
+
|
|
15
|
+
Incoming calls on Android are delivered over Firebase Cloud Messaging, so Firebase is
|
|
16
|
+
only pulled into your build when you opt in. iOS uses PushKit/APNs and needs none of this.
|
|
17
|
+
|
|
18
|
+
### Expo
|
|
19
|
+
|
|
20
|
+
Enable VoIP in the config plugin and point Expo at your `google-services.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"expo": {
|
|
25
|
+
"android": { "googleServicesFile": "./google-services.json" },
|
|
26
|
+
"plugins": [["@fishjam-cloud/react-native-client", { "android": { "enableVoIP": true } }]]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Prebuild does the rest. [`android.googleServicesFile`](https://docs.expo.dev/versions/latest/config/app/#googleservicesfile)
|
|
32
|
+
makes Expo add the `com.google.gms:google-services` classpath, apply the Gradle plugin, and copy
|
|
33
|
+
the file into `android/app/`. Omitting it while `enableVoIP` is on is a prebuild error.
|
|
34
|
+
|
|
35
|
+
### Call timeouts
|
|
36
|
+
|
|
37
|
+
The plugin can set native timeouts in seconds. An unanswered incoming call defaults
|
|
38
|
+
to 45 seconds, an unconnected outgoing call defaults to 60 seconds, and the
|
|
39
|
+
answer-fulfillment handshake defaults to 10 seconds:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"expo": {
|
|
44
|
+
"plugins": [
|
|
45
|
+
[
|
|
46
|
+
"@fishjam-cloud/react-native-client",
|
|
47
|
+
{
|
|
48
|
+
"android": { "enableVoIP": true },
|
|
49
|
+
"voip": {
|
|
50
|
+
"incomingCallTimeout": 45,
|
|
51
|
+
"outgoingCallTimeout": 60,
|
|
52
|
+
"fulfillAnswerCallTimeout": 10
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
All timeout properties are optional and must be positive finite numbers. Omit a
|
|
62
|
+
property to use its native default.
|
|
63
|
+
|
|
64
|
+
### Bare React Native
|
|
65
|
+
|
|
66
|
+
Config plugins do not run, and the [`google-services` Gradle plugin](https://developers.google.com/android/guides/google-services-plugin)
|
|
67
|
+
must be applied to the **application** module — a library cannot do it for you. Follow the
|
|
68
|
+
[Firebase Android setup guide](https://firebase.google.com/docs/android/setup) to add
|
|
69
|
+
`google-services.json` and apply the plugin.
|
|
70
|
+
|
|
71
|
+
You must also declare by hand what the config plugin would otherwise inject into
|
|
72
|
+
`AndroidManifest.xml` — the `MANAGE_OWN_CALLS`, `POST_NOTIFICATIONS`,
|
|
73
|
+
`USE_FULL_SCREEN_INTENT` and `VIBRATE` permissions, the `IncomingCallActivity`,
|
|
74
|
+
the `EndCallNotificationReceiver`, and the `PushNotificationService` with its
|
|
75
|
+
`com.google.firebase.MESSAGING_EVENT` intent filter. See `plugin/src/withFishjamVoIPAndroid.ts`
|
|
76
|
+
for the exact entries.
|
|
77
|
+
|
|
78
|
+
Set timeout metadata manually when using bare React Native:
|
|
79
|
+
|
|
80
|
+
```xml
|
|
81
|
+
<application>
|
|
82
|
+
<meta-data android:name="VoIPIncomingCallTimeout" android:value="45" />
|
|
83
|
+
<meta-data android:name="VoIPOutgoingCallTimeout" android:value="60" />
|
|
84
|
+
<meta-data android:name="VoIPFulfillAnswerTimeout" android:value="10" />
|
|
85
|
+
</application>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For iOS, add the same timeout values as numeric `Info.plist` keys:
|
|
89
|
+
`VoIPIncomingCallTimeout`, `VoIPOutgoingCallTimeout`, and
|
|
90
|
+
`VoIPFulfillAnswerTimeout`.
|
|
91
|
+
|
|
13
92
|
## Local Development with WebRTC Fork
|
|
14
93
|
|
|
15
94
|
This package depends on `@fishjam-cloud/react-native-webrtc`, a fork of `react-native-webrtc`. The fork lives in [its own GitHub repo](https://github.com/fishjam-cloud/fishjam-react-native-webrtc) and is included in this monorepo as a git submodule at `packages/react-native-webrtc/`, wired up as a yarn workspace. No manual linking is required.
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,13 @@ import './webrtc-polyfill';
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import { type FishjamProviderProps as ReactClientFishjamProviderProps } from '@fishjam-cloud/react-client';
|
|
9
9
|
export { RTCView, RTCPIPView, type RTCVideoViewProps, type RTCPIPViewProps } from './overrides/RTCView';
|
|
10
|
-
export { ScreenCapturePickerView, startPIP, stopPIP, AudioDeviceType, useAudioOutput, pushAudioSamples, } from '@fishjam-cloud/react-native-webrtc';
|
|
10
|
+
export { ScreenCapturePickerView, startPIP, stopPIP, AudioDeviceType, useAudioOutput, pushAudioSamples, useVoIPEvents, useTelecom, useTelecomEvent, fulfillIncomingCallConnected, failIncomingCallConnected, getPendingAnswerRequestId, reportOutgoingCallConnected, setCallHeld, setCallMuted, isCallHeld, } from '@fishjam-cloud/react-native-webrtc';
|
|
11
|
+
export type { VoIPEventHandlers, VoIPCallIntent, VoIPIncomingPayload } from '@fishjam-cloud/react-native-webrtc';
|
|
12
|
+
export { VoIPProvider } from './voip/VoIPProvider';
|
|
13
|
+
export type { VoIPProviderProps } from './voip/VoIPProvider';
|
|
14
|
+
export { useVoIP } from './voip/VoIPContext';
|
|
15
|
+
export type { CurrentCall, VoIPCallStatus, VoIPContextValue } from './voip/VoIPContext';
|
|
16
|
+
export type { CallEndedReason, TelecomConfig, TelecomEvent, TelecomEventType, UseTelecomResult, } from '@fishjam-cloud/react-native-webrtc';
|
|
11
17
|
export type { CallKitAction, CallKitConfig, CustomAudioSink, CustomAudioTrack, MediaStream, MediaStreamTrack, AudioDevice, AudioOutputChangedInfo, UseAudioOutputResult, } from '@fishjam-cloud/react-native-webrtc';
|
|
12
18
|
export { useForegroundService, type ForegroundServiceConfig } from './useForegroundService';
|
|
13
19
|
export { useCameraPermissions, useMicrophonePermissions, type PermissionStatus } from './hooks/usePermissions';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,mBAAmB,CAAC;AAC3B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAEL,KAAK,oBAAoB,IAAI,+BAA+B,EAC7D,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACxG,OAAO,EACL,uBAAuB,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,oCAAoC,CAAC;AAE5C,YAAY,EACV,aAAa,EACb,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/G,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,UAAU,EACV,qBAAqB,EACrB,MAAM,EACN,OAAO,GACR,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,SAAS,EACT,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,QAAQ,EACR,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,EACzB,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,EACL,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,0BAA0B,EAC1B,cAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,eAAe,EACf,eAAe,EACf,KAAK,EACL,WAAW,EACX,UAAU,EACV,uBAAuB,EACvB,MAAM,EACN,UAAU,EACV,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,OAAO,EACP,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,EACf,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAGrC,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,+BAA+B,EAAE,mBAAmB,GAAG,eAAe,CAAC,CAAC;AAChH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,mEAO1D"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,mBAAmB,CAAC;AAC3B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAEL,KAAK,oBAAoB,IAAI,+BAA+B,EAC7D,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACxG,OAAO,EACL,uBAAuB,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,eAAe,EACf,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,EACzB,2BAA2B,EAC3B,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,oCAAoC,CAAC;AAE5C,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAEjH,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAExF,YAAY,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,oCAAoC,CAAC;AAE5C,YAAY,EACV,aAAa,EACb,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/G,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,UAAU,EACV,qBAAqB,EACrB,MAAM,EACN,OAAO,GACR,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,SAAS,EACT,oBAAoB,EACpB,aAAa,EACb,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,QAAQ,EACR,UAAU,EACV,eAAe,EACf,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,EACzB,eAAe,EACf,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,EACL,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,cAAc,EACd,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,0BAA0B,EAC1B,cAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,eAAe,EACf,eAAe,EACf,KAAK,EACL,WAAW,EACX,UAAU,EACV,uBAAuB,EACvB,MAAM,EACN,UAAU,EACV,yBAAyB,EACzB,wBAAwB,EACxB,YAAY,EACZ,OAAO,EACP,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,EACf,mBAAmB,GACpB,MAAM,6BAA6B,CAAC;AAGrC,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,+BAA+B,EAAE,mBAAmB,GAAG,eAAe,CAAC,CAAC;AAChH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,mEAO1D"}
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,9 @@ import React from 'react';
|
|
|
13
13
|
import { FishjamProvider as ReactClientFishjamProvider, } from '@fishjam-cloud/react-client';
|
|
14
14
|
import { FishjamClient } from '@fishjam-cloud/ts-client';
|
|
15
15
|
export { RTCView, RTCPIPView } from './overrides/RTCView';
|
|
16
|
-
export { ScreenCapturePickerView, startPIP, stopPIP, AudioDeviceType, useAudioOutput, pushAudioSamples, } from '@fishjam-cloud/react-native-webrtc';
|
|
16
|
+
export { ScreenCapturePickerView, startPIP, stopPIP, AudioDeviceType, useAudioOutput, pushAudioSamples, useVoIPEvents, useTelecom, useTelecomEvent, fulfillIncomingCallConnected, failIncomingCallConnected, getPendingAnswerRequestId, reportOutgoingCallConnected, setCallHeld, setCallMuted, isCallHeld, } from '@fishjam-cloud/react-native-webrtc';
|
|
17
|
+
export { VoIPProvider } from './voip/VoIPProvider';
|
|
18
|
+
export { useVoIP } from './voip/VoIPContext';
|
|
17
19
|
export { useForegroundService } from './useForegroundService';
|
|
18
20
|
export { useCameraPermissions, useMicrophonePermissions } from './hooks/usePermissions';
|
|
19
21
|
export { useCustomAudioSource, } from './hooks/useCustomAudioSource';
|
|
@@ -19,8 +19,10 @@ export declare function usePeers<P = Record<string, unknown>, S = Record<string,
|
|
|
19
19
|
};
|
|
20
20
|
export declare function useCallKit(): {
|
|
21
21
|
startCallKitSession: (config: CallKitConfig) => Promise<void>;
|
|
22
|
-
endCallKitSession: () => Promise<void>;
|
|
22
|
+
endCallKitSession: (reason?: import("@fishjam-cloud/react-native-webrtc").CallEndedReason) => Promise<void>;
|
|
23
23
|
getCallKitSessionStatus: () => Promise<boolean>;
|
|
24
|
+
setCallHeld: (onHold: boolean) => Promise<void>;
|
|
25
|
+
isHeld: () => boolean;
|
|
24
26
|
};
|
|
25
27
|
export declare function useCallKitService(config: CallKitConfig): void;
|
|
26
28
|
export declare function useCallKitEvent<T extends keyof CallKitAction>(action: T, callback: (event: CallKitAction[T]) => void): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/overrides/hooks.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,oCAAoC,CAAC;AASrH,OAAO,KAAK,EAEV,uBAAuB,EACvB,cAAc,EACd,WAAW,EAGX,eAAe,EACf,2BAA2B,EAC3B,yBAAyB,EACzB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,SAAS,CAAC;AAEjB,wBAAgB,SAAS,IAAI,eAAe,CAc3C;AAED,wBAAgB,aAAa,IAAI,mBAAmB,CAcnD;AAED,wBAAgB,cAAc,IAAI,oBAAoB,CAgBrD;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,CAAC;YAIhC,aAAa,GAAG,SAAS;eACnB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC;EAEpF;AAED,wBAAgB,qBAAqB,IAAI,2BAA2B,CAYnE;AAED,wBAAgB,mBAAmB,IAAI,yBAAyB,CAM/D;AAED,wBAAgB,oBAAoB;uBAGO,CACrC,GAAG,IAAI,EAAE,UAAU,wJAAyB,KACzC,OAAO,CAAC,uBAAuB,CAAC;EAExC;AAED,wBAAgB,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC9B;IAC/C,SAAS,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACvC,WAAW,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC;IACjD,KAAK,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC;CAC5C,CACF;AAED,wBAAgB,UAAU;;;;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/overrides/hooks.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,IAAI,aAAa,EAAE,MAAM,oCAAoC,CAAC;AASrH,OAAO,KAAK,EAEV,uBAAuB,EACvB,cAAc,EACd,WAAW,EAGX,eAAe,EACf,2BAA2B,EAC3B,yBAAyB,EACzB,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,SAAS,CAAC;AAEjB,wBAAgB,SAAS,IAAI,eAAe,CAc3C;AAED,wBAAgB,aAAa,IAAI,mBAAmB,CAcnD;AAED,wBAAgB,cAAc,IAAI,oBAAoB,CAgBrD;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,CAAC;YAIhC,aAAa,GAAG,SAAS;eACnB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC;EAEpF;AAED,wBAAgB,qBAAqB,IAAI,2BAA2B,CAYnE;AAED,wBAAgB,mBAAmB,IAAI,yBAAyB,CAM/D;AAED,wBAAgB,oBAAoB;uBAGO,CACrC,GAAG,IAAI,EAAE,UAAU,wJAAyB,KACzC,OAAO,CAAC,uBAAuB,CAAC;EAExC;AAED,wBAAgB,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC9B;IAC/C,SAAS,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;IACvC,WAAW,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC;IACjD,KAAK,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,EAAE,CAAC;CAC5C,CACF;AAED,wBAAgB,UAAU;;8BAhImB,CAAC;;;;EAmI7C;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,QAEtD;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,IAAI,QAEpH"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { CallEndedReason, VoIPCallIntent } from '@fishjam-cloud/react-native-webrtc';
|
|
2
|
+
/**
|
|
3
|
+
* Lifecycle state of the current VoIP call.
|
|
4
|
+
*
|
|
5
|
+
* - `available` — no call in progress
|
|
6
|
+
* - `incoming` — a call is ringing, awaiting the user's answer
|
|
7
|
+
* - `connecting` — the call was started/answered; **your app should be joining its room now**
|
|
8
|
+
* - `active` — your app reported the media connected and the call is in progress
|
|
9
|
+
*/
|
|
10
|
+
export type VoIPCallStatus = 'available' | 'incoming' | 'connecting' | 'active';
|
|
11
|
+
/**
|
|
12
|
+
* Details of the call currently being handled.
|
|
13
|
+
*/
|
|
14
|
+
export type CurrentCall = {
|
|
15
|
+
/**
|
|
16
|
+
* Fishjam room the call takes place in. For outgoing calls it is the name passed to
|
|
17
|
+
* `startCall`; for incoming ones it comes from the VoIP push payload.
|
|
18
|
+
*/
|
|
19
|
+
roomName: string;
|
|
20
|
+
/** Name shown in the CallKit UI (the remote party). */
|
|
21
|
+
displayName: string;
|
|
22
|
+
/**
|
|
23
|
+
* Stable id of the remote party — use a durable user id, not a display name (which
|
|
24
|
+
* may not be unique), since this is what Recents hands back for redialing.
|
|
25
|
+
*/
|
|
26
|
+
handle: string;
|
|
27
|
+
/** Whether the call is a video call. */
|
|
28
|
+
isVideo: boolean;
|
|
29
|
+
/** Timestamp (ms) when the call became `active`, or `null` if not yet connected. */
|
|
30
|
+
startedAt: number | null;
|
|
31
|
+
/** `true` when this device initiated the call, `false` when receiving it. */
|
|
32
|
+
isOutgoing: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Value returned from {@link useVoIP}.
|
|
36
|
+
*/
|
|
37
|
+
export type VoIPContextValue = {
|
|
38
|
+
/** Current call lifecycle status. */
|
|
39
|
+
callStatus: VoIPCallStatus;
|
|
40
|
+
/** This device's VoIP push token, or `null` until APNs has issued one. */
|
|
41
|
+
voipToken: string | null;
|
|
42
|
+
/** The call currently being handled, or `null` when `callStatus` is `available`. */
|
|
43
|
+
currentCall: CurrentCall | null;
|
|
44
|
+
/**
|
|
45
|
+
* Why the most recently handled call ended. `null` until a call has ended at
|
|
46
|
+
* least once. Surfaced so a consumer can react to `missed`/`rejected`/etc., e.g.
|
|
47
|
+
* showing a "missed call" notification.
|
|
48
|
+
*/
|
|
49
|
+
lastEndedReason: CallEndedReason | null;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the native CallKit/Core-Telecom session is currently held. Reported only —
|
|
52
|
+
* apply it to your own tracks.
|
|
53
|
+
*/
|
|
54
|
+
isOnHold: boolean;
|
|
55
|
+
/** Whether the system call UI has the call muted. Reported only, as with {@link VoIPContextValue.isOnHold}. */
|
|
56
|
+
isMuted: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* A redial requested from the iOS **Recents** list, or `null` when there is none.
|
|
59
|
+
* It carries only the handle to call, never a room, so mint a room name yourself.
|
|
60
|
+
* Held until {@link VoIPContextValue.clearCallIntent}, so one arriving before your
|
|
61
|
+
* app has restored its session is not lost.
|
|
62
|
+
*/
|
|
63
|
+
pendingCallIntent: VoIPCallIntent | null;
|
|
64
|
+
/** Discards {@link VoIPContextValue.pendingCallIntent} once you have acted on it. */
|
|
65
|
+
clearCallIntent: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* Reports an outgoing call to `to` in `roomName` to CallKit/Core-Telecom and moves
|
|
68
|
+
* to `connecting`. Run your own signaling (ringing the callee) *before* calling this.
|
|
69
|
+
* It does **not** join the room — react to `callStatus` becoming `connecting` for that.
|
|
70
|
+
*/
|
|
71
|
+
startCall: (to: string, roomName: string) => Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Report that the room join succeeded and media is flowing. Fulfills CallKit's answer
|
|
74
|
+
* action (or reports the outgoing call as connected) and moves the call to `active`.
|
|
75
|
+
*
|
|
76
|
+
* An answered incoming call must be fulfilled within `VoIPFulfillAnswerTimeout`
|
|
77
|
+
* (10s by default) or the native side ends it and `onEnded` fires.
|
|
78
|
+
*/
|
|
79
|
+
reportConnected: () => Promise<void>;
|
|
80
|
+
/** Tell the SDK your room join failed. Ends the call with reason `failed`. */
|
|
81
|
+
reportConnectFailed: () => Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Ends or rejects the current call. Dismisses CallKit/Telecom and resets state back
|
|
84
|
+
* to `available`; leaving the room is up to you. `reason` (defaults to `local`) is
|
|
85
|
+
* surfaced to the system call UI/log and to `lastEndedReason`.
|
|
86
|
+
*/
|
|
87
|
+
endCall: (reason?: CallEndedReason) => Promise<void>;
|
|
88
|
+
/** Requests that the native CallKit/Core-Telecom session be held or resumed. */
|
|
89
|
+
setCallHeld: (onHold: boolean) => Promise<void>;
|
|
90
|
+
};
|
|
91
|
+
export declare const VoIPContext: import("react").Context<VoIPContextValue | null>;
|
|
92
|
+
/**
|
|
93
|
+
* Returns the current {@link VoIPContextValue}.
|
|
94
|
+
*
|
|
95
|
+
* Must be used inside a `VoIPProvider`. Without it the VoIP call machine is not
|
|
96
|
+
* mounted and this hook throws.
|
|
97
|
+
*/
|
|
98
|
+
export declare function useVoIP(): VoIPContextValue;
|
|
99
|
+
//# sourceMappingURL=VoIPContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoIPContext.d.ts","sourceRoot":"","sources":["../../src/voip/VoIPContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAG1F;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEhF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qCAAqC;IACrC,UAAU,EAAE,cAAc,CAAC;IAC3B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oFAAoF;IACpF,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC;;;;OAIG;IACH,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,+GAA+G;IAC/G,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,iBAAiB,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,qFAAqF;IACrF,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B;;;;OAIG;IACH,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D;;;;;;OAMG;IACH,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,8EAA8E;IAC9E,mBAAmB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,gFAAgF;IAChF,WAAW,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD,CAAC;AAEF,eAAO,MAAM,WAAW,kDAA+C,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,gBAAgB,CAQ1C"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const VoIPContext = createContext(null);
|
|
3
|
+
/**
|
|
4
|
+
* Returns the current {@link VoIPContextValue}.
|
|
5
|
+
*
|
|
6
|
+
* Must be used inside a `VoIPProvider`. Without it the VoIP call machine is not
|
|
7
|
+
* mounted and this hook throws.
|
|
8
|
+
*/
|
|
9
|
+
export function useVoIP() {
|
|
10
|
+
const ctx = useContext(VoIPContext);
|
|
11
|
+
if (!ctx) {
|
|
12
|
+
throw new Error('useVoIP must be used inside a VoIPProvider — wrap your app in `<VoIPProvider>` to enable VoIP calls.');
|
|
13
|
+
}
|
|
14
|
+
return ctx;
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type VoIPIncomingPayload } from '@fishjam-cloud/react-native-webrtc';
|
|
2
|
+
import { type PropsWithChildren } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Props of {@link VoIPProvider} — the configuration of the VoIP call machine.
|
|
5
|
+
*/
|
|
6
|
+
export type VoIPProviderProps = PropsWithChildren & {
|
|
7
|
+
/**
|
|
8
|
+
* A waiting or overflow incoming call was declined from native UI. Does not
|
|
9
|
+
* change local call state - use for signaling (e.g. `call-rejected` to the caller).
|
|
10
|
+
*/
|
|
11
|
+
onWaitingCallDeclined?: (payload: VoIPIncomingPayload) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Whether outgoing calls are video calls — reflected in the CallKit session.
|
|
14
|
+
* Make sure the underlying room type is set accordingly. Defaults to `false` (audio-only).
|
|
15
|
+
*/
|
|
16
|
+
isVideo?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Tracks the current VoIP call state, driven by the native CallKit / Core-Telecom
|
|
20
|
+
* events from {@link useVoIPEvents}, and exposes it through {@link useVoIP}.
|
|
21
|
+
*
|
|
22
|
+
* Joining rooms, peer tokens and media are the consumer's — react to `callStatus` and
|
|
23
|
+
* report back with `reportConnected` / `reportConnectFailed`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function VoIPProvider({ onWaitingCallDeclined, isVideo, children }: VoIPProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
//# sourceMappingURL=VoIPProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoIPProvider.d.ts","sourceRoot":"","sources":["../../src/voip/VoIPProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,mBAAmB,EACzB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,KAAK,iBAAiB,EAA0C,MAAM,OAAO,CAAC;AAMvF;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG;IAClD;;;OAGG;IACH,qBAAqB,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC/D;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,EAAE,qBAAqB,EAAE,OAAe,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CAiTnG"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { failIncomingCallConnected, fulfillIncomingCallConnected, reportOutgoingCallConnected, setCallHeld as setVoIPCallHeld, useTelecom, useVoIPEvents, } from '@fishjam-cloud/react-native-webrtc';
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Platform } from 'react-native';
|
|
5
|
+
import { useCallKit } from '../overrides/hooks';
|
|
6
|
+
import { VoIPContext } from './VoIPContext';
|
|
7
|
+
/**
|
|
8
|
+
* Tracks the current VoIP call state, driven by the native CallKit / Core-Telecom
|
|
9
|
+
* events from {@link useVoIPEvents}, and exposes it through {@link useVoIP}.
|
|
10
|
+
*
|
|
11
|
+
* Joining rooms, peer tokens and media are the consumer's — react to `callStatus` and
|
|
12
|
+
* report back with `reportConnected` / `reportConnectFailed`.
|
|
13
|
+
*/
|
|
14
|
+
export function VoIPProvider({ onWaitingCallDeclined, isVideo = false, children }) {
|
|
15
|
+
const [voipToken, setVoIPToken] = useState(null);
|
|
16
|
+
const [callStatus, setCallStatus] = useState('available');
|
|
17
|
+
const [currentCall, setCurrentCall] = useState(null);
|
|
18
|
+
const [lastEndedReason, setLastEndedReason] = useState(null);
|
|
19
|
+
const [isOnHold, setIsOnHold] = useState(false);
|
|
20
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
21
|
+
const [pendingCallIntent, setPendingCallIntent] = useState(null);
|
|
22
|
+
const currentCallRef = useRef(null);
|
|
23
|
+
const pendingAnswerRequestIdRef = useRef(null);
|
|
24
|
+
const activationInFlightRef = useRef(false);
|
|
25
|
+
const isCallOnHoldRef = useRef(false);
|
|
26
|
+
/** Serializes native call events so End & Accept cannot interleave their transitions. */
|
|
27
|
+
const callTransitionRef = useRef(Promise.resolve());
|
|
28
|
+
/**
|
|
29
|
+
* Set when the accept (`onAnswered`) is processed before the promoted waiting
|
|
30
|
+
* call's `onIncoming` payload, so `onIncoming` can replay the answer.
|
|
31
|
+
*/
|
|
32
|
+
const pendingWaitingAnswerRef = useRef(null);
|
|
33
|
+
const enqueueCallTransition = useCallback((op) => {
|
|
34
|
+
const run = callTransitionRef.current.then(op);
|
|
35
|
+
callTransitionRef.current = run.catch(() => { });
|
|
36
|
+
return run;
|
|
37
|
+
}, []);
|
|
38
|
+
const { startCallKitSession, endCallKitSession } = useCallKit();
|
|
39
|
+
const { startCall: startTelecomSession, endCall: endTelecomSession } = useTelecom();
|
|
40
|
+
const startNativeCallSession = useCallback((to) => Platform.OS === 'ios'
|
|
41
|
+
? startCallKitSession({ displayName: to, handle: to, isVideo })
|
|
42
|
+
: startTelecomSession({ displayName: to, handle: to, isVideo }), [startCallKitSession, startTelecomSession, isVideo]);
|
|
43
|
+
const endNativeCallSession = useCallback((reason) => (Platform.OS === 'ios' ? endCallKitSession(reason) : endTelecomSession(reason)), [endCallKitSession, endTelecomSession]);
|
|
44
|
+
const setCallHeld = useCallback(async (onHold) => {
|
|
45
|
+
if (!currentCallRef.current) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await setVoIPCallHeld(onHold);
|
|
49
|
+
}, []);
|
|
50
|
+
const resetCallState = useCallback((reason = 'local', endedRoomName) => {
|
|
51
|
+
const roomName = endedRoomName ?? currentCallRef.current?.roomName;
|
|
52
|
+
if (!roomName || currentCallRef.current?.roomName !== roomName) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
currentCallRef.current = null;
|
|
56
|
+
pendingAnswerRequestIdRef.current = null;
|
|
57
|
+
pendingWaitingAnswerRef.current = null;
|
|
58
|
+
isCallOnHoldRef.current = false;
|
|
59
|
+
setIsOnHold(false);
|
|
60
|
+
setIsMuted(false);
|
|
61
|
+
setCurrentCall(null);
|
|
62
|
+
setCallStatus('available');
|
|
63
|
+
setLastEndedReason(reason);
|
|
64
|
+
}, []);
|
|
65
|
+
const endCall = useCallback(async (reason = 'local', options) => {
|
|
66
|
+
const endedCall = currentCallRef.current;
|
|
67
|
+
if (!endedCall)
|
|
68
|
+
return;
|
|
69
|
+
resetCallState(reason, endedCall.roomName);
|
|
70
|
+
// Native already ended the CallKit session before `onEnded` fired — calling
|
|
71
|
+
// endNativeCallSession again during call-waiting swap would end the new call.
|
|
72
|
+
if (!options?.fromNative) {
|
|
73
|
+
await endNativeCallSession(reason);
|
|
74
|
+
}
|
|
75
|
+
}, [endNativeCallSession, resetCallState]);
|
|
76
|
+
const startCall = useCallback(async (to, roomName) => {
|
|
77
|
+
const call = {
|
|
78
|
+
roomName,
|
|
79
|
+
displayName: to,
|
|
80
|
+
handle: to,
|
|
81
|
+
isVideo,
|
|
82
|
+
startedAt: null,
|
|
83
|
+
isOutgoing: true,
|
|
84
|
+
};
|
|
85
|
+
currentCallRef.current = call;
|
|
86
|
+
setCurrentCall(call);
|
|
87
|
+
setCallStatus('connecting');
|
|
88
|
+
setLastEndedReason(null);
|
|
89
|
+
try {
|
|
90
|
+
await startNativeCallSession(to);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error('Failed to start call:', err);
|
|
94
|
+
await endCall('failed');
|
|
95
|
+
}
|
|
96
|
+
}, [startNativeCallSession, isVideo, endCall]);
|
|
97
|
+
const reportConnected = useCallback(async () => {
|
|
98
|
+
const call = currentCallRef.current;
|
|
99
|
+
if (!call || call.startedAt != null || activationInFlightRef.current) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
activationInFlightRef.current = true;
|
|
103
|
+
try {
|
|
104
|
+
const requestId = pendingAnswerRequestIdRef.current;
|
|
105
|
+
if (requestId) {
|
|
106
|
+
pendingAnswerRequestIdRef.current = null;
|
|
107
|
+
const connected = await fulfillIncomingCallConnected(requestId);
|
|
108
|
+
if (!connected) {
|
|
109
|
+
if (currentCallRef.current === call) {
|
|
110
|
+
await endCall('failed');
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (call.isOutgoing) {
|
|
116
|
+
await reportOutgoingCallConnected();
|
|
117
|
+
}
|
|
118
|
+
// The call may have ended while we were talking to the native side.
|
|
119
|
+
const activeCall = currentCallRef.current;
|
|
120
|
+
if (!activeCall || activeCall.roomName !== call.roomName)
|
|
121
|
+
return;
|
|
122
|
+
const connectedCall = { ...activeCall, startedAt: Date.now() };
|
|
123
|
+
currentCallRef.current = connectedCall;
|
|
124
|
+
setCurrentCall(connectedCall);
|
|
125
|
+
setCallStatus('active');
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error('Failed to activate call:', err);
|
|
129
|
+
if (currentCallRef.current === call) {
|
|
130
|
+
await endCall('failed');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
activationInFlightRef.current = false;
|
|
135
|
+
}
|
|
136
|
+
}, [endCall]);
|
|
137
|
+
const reportConnectFailed = useCallback(async () => {
|
|
138
|
+
const call = currentCallRef.current;
|
|
139
|
+
if (!call)
|
|
140
|
+
return;
|
|
141
|
+
const requestId = pendingAnswerRequestIdRef.current;
|
|
142
|
+
if (!requestId) {
|
|
143
|
+
await endCall('failed');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
pendingAnswerRequestIdRef.current = null;
|
|
147
|
+
try {
|
|
148
|
+
await failIncomingCallConnected(requestId);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
resetCallState('failed', call.roomName);
|
|
152
|
+
}
|
|
153
|
+
}, [endCall, resetCallState]);
|
|
154
|
+
const clearCallIntent = useCallback(() => setPendingCallIntent(null), []);
|
|
155
|
+
useVoIPEvents({
|
|
156
|
+
onRegistered: useCallback((token) => {
|
|
157
|
+
setVoIPToken(token);
|
|
158
|
+
}, []),
|
|
159
|
+
// Native only delivers `onIncoming` for a *first* call, or for a waiting call
|
|
160
|
+
// the moment the user picks "End & Accept" — never while a waiting call merely
|
|
161
|
+
// rings (that stays entirely inside CallKit/Telecom). So a payload arriving
|
|
162
|
+
// while we're already in a call means an accepted waiting call is taking over:
|
|
163
|
+
// native has already ended the old one, and we just swap `currentCall` over.
|
|
164
|
+
// Leaving the old room and joining the new one falls out of the consumer
|
|
165
|
+
// reacting to `currentCall.roomName` changing.
|
|
166
|
+
onIncoming: useCallback((payload) => {
|
|
167
|
+
enqueueCallTransition(async () => {
|
|
168
|
+
const call = {
|
|
169
|
+
roomName: payload.roomName,
|
|
170
|
+
displayName: payload.displayName,
|
|
171
|
+
handle: payload.handle,
|
|
172
|
+
isVideo: payload.isVideo,
|
|
173
|
+
startedAt: null,
|
|
174
|
+
isOutgoing: false,
|
|
175
|
+
};
|
|
176
|
+
currentCallRef.current = call;
|
|
177
|
+
pendingAnswerRequestIdRef.current = null;
|
|
178
|
+
isCallOnHoldRef.current = false;
|
|
179
|
+
setCurrentCall(call);
|
|
180
|
+
setCallStatus('incoming');
|
|
181
|
+
setLastEndedReason(null);
|
|
182
|
+
setIsOnHold(false);
|
|
183
|
+
setIsMuted(false);
|
|
184
|
+
// The user already accepted this call before its payload arrived — apply
|
|
185
|
+
// the answer we stashed then.
|
|
186
|
+
const pendingAnswer = pendingWaitingAnswerRef.current;
|
|
187
|
+
if (pendingAnswer) {
|
|
188
|
+
pendingWaitingAnswerRef.current = null;
|
|
189
|
+
pendingAnswerRequestIdRef.current = pendingAnswer;
|
|
190
|
+
setCallStatus('connecting');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}, [enqueueCallTransition]),
|
|
194
|
+
onAnswered: useCallback((requestId) => {
|
|
195
|
+
enqueueCallTransition(async () => {
|
|
196
|
+
if (pendingAnswerRequestIdRef.current) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// No ringing call to answer yet: either the accepted waiting call's
|
|
200
|
+
// `onIncoming` hasn't arrived, or `call` is still the old (already
|
|
201
|
+
// `active`) call that native is ending. Stash so `onIncoming` replays it.
|
|
202
|
+
const call = currentCallRef.current;
|
|
203
|
+
if (!call || call.startedAt != null) {
|
|
204
|
+
pendingWaitingAnswerRef.current = requestId;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
pendingAnswerRequestIdRef.current = requestId;
|
|
208
|
+
setCallStatus('connecting');
|
|
209
|
+
});
|
|
210
|
+
}, [enqueueCallTransition]),
|
|
211
|
+
onEnded: useCallback((reason) => {
|
|
212
|
+
enqueueCallTransition(async () => {
|
|
213
|
+
await endCall(reason ?? 'remote', { fromNative: true });
|
|
214
|
+
});
|
|
215
|
+
}, [endCall, enqueueCallTransition]),
|
|
216
|
+
onHeldChanged: useCallback((onHold) => {
|
|
217
|
+
if (!currentCallRef.current?.startedAt || isCallOnHoldRef.current === onHold) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
isCallOnHoldRef.current = onHold;
|
|
221
|
+
setIsOnHold(onHold);
|
|
222
|
+
}, []),
|
|
223
|
+
onMuteChanged: useCallback((muted) => {
|
|
224
|
+
if (!currentCallRef.current?.startedAt) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
setIsMuted(muted);
|
|
228
|
+
}, []),
|
|
229
|
+
onCallIntent: useCallback((intent) => {
|
|
230
|
+
if (currentCallRef.current) {
|
|
231
|
+
console.warn('Ignoring call intent while another call is active');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
setPendingCallIntent(intent);
|
|
235
|
+
}, []),
|
|
236
|
+
onWaitingCallDeclined,
|
|
237
|
+
});
|
|
238
|
+
const voipValue = useMemo(() => ({
|
|
239
|
+
voipToken,
|
|
240
|
+
callStatus,
|
|
241
|
+
currentCall,
|
|
242
|
+
lastEndedReason,
|
|
243
|
+
isOnHold,
|
|
244
|
+
isMuted,
|
|
245
|
+
pendingCallIntent,
|
|
246
|
+
clearCallIntent,
|
|
247
|
+
startCall,
|
|
248
|
+
reportConnected,
|
|
249
|
+
reportConnectFailed,
|
|
250
|
+
endCall,
|
|
251
|
+
setCallHeld,
|
|
252
|
+
}), [
|
|
253
|
+
voipToken,
|
|
254
|
+
callStatus,
|
|
255
|
+
currentCall,
|
|
256
|
+
lastEndedReason,
|
|
257
|
+
isOnHold,
|
|
258
|
+
isMuted,
|
|
259
|
+
pendingCallIntent,
|
|
260
|
+
clearCallIntent,
|
|
261
|
+
startCall,
|
|
262
|
+
reportConnected,
|
|
263
|
+
reportConnectFailed,
|
|
264
|
+
endCall,
|
|
265
|
+
setCallHeld,
|
|
266
|
+
]);
|
|
267
|
+
return _jsx(VoIPContext.Provider, { value: voipValue, children: children });
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fishjam-cloud/react-native-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0-rc.1",
|
|
4
4
|
"description": "React Native client library for Fishjam",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Fishjam Team",
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@fishjam-cloud/react-native-webrtc": "0.
|
|
45
|
+
"@fishjam-cloud/react-native-webrtc": "0.30.0-rc.0",
|
|
46
46
|
"expo": "*",
|
|
47
47
|
"react": "*",
|
|
48
48
|
"react-native": "*",
|
|
49
49
|
"react-native-get-random-values": "1.11.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@fishjam-cloud/react-client": "0.
|
|
53
|
-
"@fishjam-cloud/react-native-webrtc": "0.
|
|
52
|
+
"@fishjam-cloud/react-client": "0.30.0-rc.1",
|
|
53
|
+
"@fishjam-cloud/react-native-webrtc": "0.30.0-rc.0",
|
|
54
54
|
"fast-text-encoding": "1.0.6",
|
|
55
55
|
"react-native-get-random-values": "1.11.0",
|
|
56
56
|
"react-native-url-polyfill": "3.0.0"
|
package/plugin/build/types.d.ts
CHANGED
|
@@ -3,6 +3,21 @@ export type FishjamPluginOptions = {
|
|
|
3
3
|
enableForegroundService?: boolean;
|
|
4
4
|
enableScreensharing?: boolean;
|
|
5
5
|
supportsPictureInPicture?: boolean;
|
|
6
|
+
enableVoIP?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Register the SDK's FCM messaging service (default `true`). Set to `false`
|
|
9
|
+
* when the app ships its own dispatcher service that calls
|
|
10
|
+
* `PushNotificationService.handleVoIPMessage` / `handleNewToken`.
|
|
11
|
+
*/
|
|
12
|
+
voipMessagingService?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Messaging service that non-VoIP FCM messages and token callbacks are
|
|
15
|
+
* relayed to. Pass a supported library name (`'expo-notifications'`,
|
|
16
|
+
* `'@react-native-firebase/messaging'`) or a fully-qualified service class
|
|
17
|
+
* name. Omit to disable relaying — without it, other push libraries won't
|
|
18
|
+
* receive messages while VoIP is enabled.
|
|
19
|
+
*/
|
|
20
|
+
voipFallbackMessagingService?: string;
|
|
6
21
|
};
|
|
7
22
|
ios?: {
|
|
8
23
|
enableScreensharing?: boolean;
|
|
@@ -14,4 +29,10 @@ export type FishjamPluginOptions = {
|
|
|
14
29
|
iphoneDeploymentTarget?: string;
|
|
15
30
|
enableVoIPBackgroundMode?: boolean;
|
|
16
31
|
};
|
|
32
|
+
voip?: {
|
|
33
|
+
incomingCallTimeout?: number;
|
|
34
|
+
outgoingCallTimeout?: number;
|
|
35
|
+
fulfillAnswerCallTimeout?: number;
|
|
36
|
+
notificationIcon?: string;
|
|
37
|
+
};
|
|
17
38
|
} | undefined;
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.withFishjamAndroid = void 0;
|
|
4
4
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
5
5
|
const Manifest_1 = require("@expo/config-plugins/build/android/Manifest");
|
|
6
|
+
const withFishjamVoIPAndroid_1 = require("./withFishjamVoIPAndroid");
|
|
7
|
+
const needsForegroundService = (props) => Boolean(props?.android?.enableForegroundService || props?.android?.enableVoIP);
|
|
6
8
|
const withFishjamPictureInPicture = (config, props) => (0, config_plugins_1.withAndroidManifest)(config, (configuration) => {
|
|
7
9
|
const activity = config_plugins_1.AndroidConfig.Manifest.getMainActivityOrThrow(configuration.modResults);
|
|
8
10
|
if (props?.android?.supportsPictureInPicture) {
|
|
@@ -14,7 +16,7 @@ const withFishjamPictureInPicture = (config, props) => (0, config_plugins_1.with
|
|
|
14
16
|
return configuration;
|
|
15
17
|
});
|
|
16
18
|
const withFishjamForegroundService = (config, props) => (0, config_plugins_1.withAndroidManifest)(config, async (configuration) => {
|
|
17
|
-
if (!props
|
|
19
|
+
if (!needsForegroundService(props)) {
|
|
18
20
|
return configuration;
|
|
19
21
|
}
|
|
20
22
|
const mainApplication = (0, Manifest_1.getMainApplicationOrThrow)(configuration.modResults);
|
|
@@ -39,7 +41,7 @@ const withFishjamForegroundService = (config, props) => (0, config_plugins_1.wit
|
|
|
39
41
|
return configuration;
|
|
40
42
|
});
|
|
41
43
|
const withFishjamForegroundServicePermission = (config, props) => (0, config_plugins_1.withAndroidManifest)(config, (configuration) => {
|
|
42
|
-
if (!props
|
|
44
|
+
if (!needsForegroundService(props)) {
|
|
43
45
|
return configuration;
|
|
44
46
|
}
|
|
45
47
|
const mainApplication = configuration.modResults;
|
|
@@ -71,6 +73,7 @@ const withFishjamForegroundServicePermission = (config, props) => (0, config_plu
|
|
|
71
73
|
const withFishjamAndroid = (config, props) => {
|
|
72
74
|
config = withFishjamForegroundServicePermission(config, props);
|
|
73
75
|
config = withFishjamForegroundService(config, props);
|
|
76
|
+
config = (0, withFishjamVoIPAndroid_1.withFishjamVoIPAndroid)(config, props);
|
|
74
77
|
config = withFishjamPictureInPicture(config, props);
|
|
75
78
|
return config;
|
|
76
79
|
};
|
|
@@ -258,6 +258,59 @@ const withFishjamVoIPBackgroundMode = (config, props) => (0, config_plugins_1.wi
|
|
|
258
258
|
}
|
|
259
259
|
return configuration;
|
|
260
260
|
});
|
|
261
|
+
const withFishjamVoIPTimeouts = (config, props) => (0, config_plugins_1.withInfoPlist)(config, (configuration) => {
|
|
262
|
+
const timeouts = [
|
|
263
|
+
['VoIPIncomingCallTimeout', 'incomingCallTimeout'],
|
|
264
|
+
['VoIPOutgoingCallTimeout', 'outgoingCallTimeout'],
|
|
265
|
+
['VoIPFulfillAnswerTimeout', 'fulfillAnswerCallTimeout'],
|
|
266
|
+
];
|
|
267
|
+
timeouts.forEach(([key, option]) => {
|
|
268
|
+
const seconds = props?.voip?.[option];
|
|
269
|
+
if (seconds === undefined) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
273
|
+
throw new Error(`Fishjam VoIP ${option} must be a positive finite number of seconds.`);
|
|
274
|
+
}
|
|
275
|
+
configuration.modResults[key] = Math.floor(seconds);
|
|
276
|
+
});
|
|
277
|
+
return configuration;
|
|
278
|
+
});
|
|
279
|
+
const withFishjamExpoVoip = (config, props) => {
|
|
280
|
+
if (!props?.voip) {
|
|
281
|
+
return config;
|
|
282
|
+
}
|
|
283
|
+
return (0, config_plugins_1.withInfoPlist)(config, (configuration) => {
|
|
284
|
+
try {
|
|
285
|
+
require.resolve('@fishjam-cloud/ios-expo-voip/package.json', {
|
|
286
|
+
paths: [configuration.modRequest.projectRoot],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw new Error('Fishjam VoIP options are enabled but @fishjam-cloud/ios-expo-voip is not installed. ' +
|
|
291
|
+
'Run: npx expo install @fishjam-cloud/ios-expo-voip');
|
|
292
|
+
}
|
|
293
|
+
configuration.modResults['FishjamVoIPEnabled'] = true;
|
|
294
|
+
return configuration;
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
const withFishjamVoIPRecentsAndIntents = (config, props) => {
|
|
298
|
+
if (!props?.voip) {
|
|
299
|
+
return config;
|
|
300
|
+
}
|
|
301
|
+
return (0, config_plugins_1.withInfoPlist)(config, (configuration) => {
|
|
302
|
+
const activityTypes = new Set(Array.isArray(configuration.modResults.NSUserActivityTypes)
|
|
303
|
+
? configuration.modResults.NSUserActivityTypes
|
|
304
|
+
: []);
|
|
305
|
+
// The audio/video variants are deprecated in favour of INStartCallIntent, but Recents
|
|
306
|
+
// redial still delivers them, so all three must be declared.
|
|
307
|
+
activityTypes.add('INStartCallIntent');
|
|
308
|
+
activityTypes.add('INStartAudioCallIntent');
|
|
309
|
+
activityTypes.add('INStartVideoCallIntent');
|
|
310
|
+
configuration.modResults.NSUserActivityTypes = Array.from(activityTypes);
|
|
311
|
+
return configuration;
|
|
312
|
+
});
|
|
313
|
+
};
|
|
261
314
|
const withFishjamPictureInPicture = (config, props) => (0, config_plugins_1.withInfoPlist)(config, (configuration) => {
|
|
262
315
|
if (props?.ios?.supportsPictureInPicture) {
|
|
263
316
|
const backgroundModes = new Set(configuration.modResults.UIBackgroundModes ?? []);
|
|
@@ -278,6 +331,9 @@ const withFishjamIos = (config, props) => {
|
|
|
278
331
|
});
|
|
279
332
|
config = withFishjamPictureInPicture(config, props);
|
|
280
333
|
config = withFishjamVoIPBackgroundMode(config, props);
|
|
334
|
+
config = withFishjamVoIPTimeouts(config, props);
|
|
335
|
+
config = withFishjamExpoVoip(config, props);
|
|
336
|
+
config = withFishjamVoIPRecentsAndIntents(config, props);
|
|
281
337
|
return config;
|
|
282
338
|
};
|
|
283
339
|
exports.withFishjamIos = withFishjamIos;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withFishjamVoIPAndroid = void 0;
|
|
4
|
+
const config_plugins_1 = require("@expo/config-plugins");
|
|
5
|
+
const Manifest_1 = require("@expo/config-plugins/build/android/Manifest");
|
|
6
|
+
/**
|
|
7
|
+
* Manifest entries required by the Android Telecom (VoIP calling) integration.
|
|
8
|
+
* Opt in with `android.enableVoIP`.
|
|
9
|
+
*
|
|
10
|
+
* - MANAGE_OWN_CALLS: required to register calls with Telecom via
|
|
11
|
+
* androidx.core.telecom CallsManager.
|
|
12
|
+
* - POST_NOTIFICATIONS: the incoming/ongoing CallStyle notifications.
|
|
13
|
+
* - USE_FULL_SCREEN_INTENT: the incoming-call ring screen over the lock screen.
|
|
14
|
+
* - VIBRATE: the looping ring vibration driven while an incoming call rings.
|
|
15
|
+
*/
|
|
16
|
+
const VOIP_PERMISSIONS = [
|
|
17
|
+
'android.permission.MANAGE_OWN_CALLS',
|
|
18
|
+
'android.permission.POST_NOTIFICATIONS',
|
|
19
|
+
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
20
|
+
'android.permission.VIBRATE',
|
|
21
|
+
];
|
|
22
|
+
const INCOMING_CALL_ACTIVITY = {
|
|
23
|
+
$: {
|
|
24
|
+
'android:name': 'com.oney.WebRTCModule.voip.IncomingCallActivity',
|
|
25
|
+
'android:exported': 'false',
|
|
26
|
+
'android:showWhenLocked': 'true',
|
|
27
|
+
'android:turnScreenOn': 'true',
|
|
28
|
+
'android:launchMode': 'singleInstance',
|
|
29
|
+
'android:excludeFromRecents': 'true',
|
|
30
|
+
'android:taskAffinity': '',
|
|
31
|
+
'android:theme': '@android:style/Theme.Black.NoTitleBar.Fullscreen',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const END_CALL_RECEIVER = {
|
|
35
|
+
$: {
|
|
36
|
+
'android:name': 'com.oney.WebRTCModule.voip.EndCallNotificationReceiver',
|
|
37
|
+
'android:exported': 'false',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const MESSAGING_SERVICE = {
|
|
41
|
+
'$': {
|
|
42
|
+
'android:name': 'com.oney.WebRTCModule.voip.PushNotificationService',
|
|
43
|
+
'android:exported': 'false',
|
|
44
|
+
},
|
|
45
|
+
'intent-filter': [
|
|
46
|
+
{
|
|
47
|
+
$: { 'android:priority': '1' },
|
|
48
|
+
action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const INSTALLATION_ID_META = {
|
|
53
|
+
$: {
|
|
54
|
+
'android:name': 'firebase_messaging_installation_id_enabled',
|
|
55
|
+
'android:value': 'true',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const FALLBACK_META_NAME = 'VoIPFallbackMessagingService';
|
|
59
|
+
/**
|
|
60
|
+
* Library names accepted by `android.voipFallbackMessagingService`, mapped to
|
|
61
|
+
* their FCM service class. These services come from **library** manifests, so on
|
|
62
|
+
* top of configuring the relay we re-declare them with tools:node="replace" and
|
|
63
|
+
* no intent-filter: the library's MESSAGING_EVENT filter is dropped (FCM routing
|
|
64
|
+
* stays deterministic) while the manifest entry keeps R8 from stripping the
|
|
65
|
+
* reflectively-loaded class in minified builds.
|
|
66
|
+
*/
|
|
67
|
+
const KNOWN_FALLBACK_SERVICES = {
|
|
68
|
+
'expo-notifications': 'expo.modules.notifications.service.ExpoFirebaseMessagingService',
|
|
69
|
+
'@react-native-firebase/messaging': 'io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService',
|
|
70
|
+
};
|
|
71
|
+
const VOIP_TIMEOUTS = [
|
|
72
|
+
['VoIPIncomingCallTimeout', 'incomingCallTimeout'],
|
|
73
|
+
['VoIPOutgoingCallTimeout', 'outgoingCallTimeout'],
|
|
74
|
+
['VoIPFulfillAnswerTimeout', 'fulfillAnswerCallTimeout'],
|
|
75
|
+
];
|
|
76
|
+
const NOTIFICATION_ICON_META_NAME = 'VoIPNotificationIcon';
|
|
77
|
+
// The CallStyle notification's small icon defaults to the app's launcher icon.
|
|
78
|
+
const DEFAULT_NOTIFICATION_ICON = '@mipmap/ic_launcher';
|
|
79
|
+
function validateTimeout(name, seconds) {
|
|
80
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
81
|
+
throw new Error(`Fishjam VoIP ${name} must be a positive finite number of seconds.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resolveFallbackMessagingService(props) {
|
|
85
|
+
const option = props?.android?.voipFallbackMessagingService;
|
|
86
|
+
if (!option) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const known = KNOWN_FALLBACK_SERVICES[option];
|
|
90
|
+
if (known) {
|
|
91
|
+
return { className: known, knownLibrary: true };
|
|
92
|
+
}
|
|
93
|
+
if (option.includes('.')) {
|
|
94
|
+
return { className: option, knownLibrary: false };
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Fishjam VoIP: unknown voipFallbackMessagingService '${option}'. ` +
|
|
97
|
+
`Use one of ${Object.keys(KNOWN_FALLBACK_SERVICES).join(', ')} ` +
|
|
98
|
+
`or a fully-qualified FirebaseMessagingService class name.`);
|
|
99
|
+
}
|
|
100
|
+
/** Applies every VoIP manifest change. */
|
|
101
|
+
function applyVoIPManifest(androidManifest, props, fallbackService) {
|
|
102
|
+
const manifest = androidManifest.manifest;
|
|
103
|
+
if (!manifest['uses-permission']) {
|
|
104
|
+
manifest['uses-permission'] = [];
|
|
105
|
+
}
|
|
106
|
+
const permissions = manifest['uses-permission'];
|
|
107
|
+
VOIP_PERMISSIONS.forEach((permissionName) => {
|
|
108
|
+
const hasPermission = permissions.some((perm) => perm.$?.['android:name'] === permissionName);
|
|
109
|
+
if (!hasPermission) {
|
|
110
|
+
permissions.push({ $: { 'android:name': permissionName } });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const mainApplication = (0, Manifest_1.getMainApplicationOrThrow)(androidManifest);
|
|
114
|
+
mainApplication.activity = mainApplication.activity || [];
|
|
115
|
+
const activityName = INCOMING_CALL_ACTIVITY.$['android:name'];
|
|
116
|
+
const existingActivityIndex = mainApplication.activity.findIndex((activity) => activity.$['android:name'] === activityName);
|
|
117
|
+
if (existingActivityIndex !== -1) {
|
|
118
|
+
mainApplication.activity[existingActivityIndex] = INCOMING_CALL_ACTIVITY;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
mainApplication.activity.push(INCOMING_CALL_ACTIVITY);
|
|
122
|
+
}
|
|
123
|
+
mainApplication.receiver = mainApplication.receiver || [];
|
|
124
|
+
const receiverName = END_CALL_RECEIVER.$['android:name'];
|
|
125
|
+
const existingReceiverIndex = mainApplication.receiver.findIndex((receiver) => receiver.$['android:name'] === receiverName);
|
|
126
|
+
if (existingReceiverIndex !== -1) {
|
|
127
|
+
mainApplication.receiver[existingReceiverIndex] = END_CALL_RECEIVER;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
mainApplication.receiver.push(END_CALL_RECEIVER);
|
|
131
|
+
}
|
|
132
|
+
mainApplication.service = mainApplication.service || [];
|
|
133
|
+
if (props?.android?.voipMessagingService !== false) {
|
|
134
|
+
// Cast: ManifestIntentFilter's typing omits android:priority, which is valid XML.
|
|
135
|
+
upsertService(mainApplication.service, MESSAGING_SERVICE);
|
|
136
|
+
if (fallbackService?.knownLibrary) {
|
|
137
|
+
manifest.$['xmlns:tools'] = manifest.$['xmlns:tools'] ?? 'http://schemas.android.com/tools';
|
|
138
|
+
upsertService(mainApplication.service, {
|
|
139
|
+
$: {
|
|
140
|
+
'android:name': fallbackService.className,
|
|
141
|
+
'tools:node': 'replace',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const metadataEntries = mainApplication['meta-data'] || [];
|
|
147
|
+
mainApplication['meta-data'] = metadataEntries;
|
|
148
|
+
const upsertMeta = (entry) => {
|
|
149
|
+
const existingIndex = metadataEntries.findIndex((meta) => meta.$['android:name'] === entry.$['android:name']);
|
|
150
|
+
if (existingIndex !== -1) {
|
|
151
|
+
metadataEntries[existingIndex] = entry;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
metadataEntries.push(entry);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
upsertMeta(INSTALLATION_ID_META);
|
|
158
|
+
if (props?.android?.voipMessagingService !== false && fallbackService) {
|
|
159
|
+
upsertMeta({
|
|
160
|
+
$: {
|
|
161
|
+
'android:name': FALLBACK_META_NAME,
|
|
162
|
+
'android:value': fallbackService.className,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
VOIP_TIMEOUTS.forEach(([key, option]) => {
|
|
167
|
+
const seconds = props?.voip?.[option];
|
|
168
|
+
if (seconds === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
validateTimeout(option, seconds);
|
|
172
|
+
upsertMeta({
|
|
173
|
+
$: {
|
|
174
|
+
'android:name': key,
|
|
175
|
+
'android:value': String(Math.floor(seconds)),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// CallStyle notification small icon; defaults to the app's launcher icon.
|
|
180
|
+
upsertMeta({
|
|
181
|
+
$: {
|
|
182
|
+
'android:name': NOTIFICATION_ICON_META_NAME,
|
|
183
|
+
'android:resource': props?.voip?.notificationIcon ?? DEFAULT_NOTIFICATION_ICON,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return androidManifest;
|
|
187
|
+
}
|
|
188
|
+
function upsertService(services, entry) {
|
|
189
|
+
const existingIndex = services.findIndex((service) => service.$?.['android:name'] === entry.$?.['android:name']);
|
|
190
|
+
if (existingIndex !== -1) {
|
|
191
|
+
services[existingIndex] = entry;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
services.push(entry);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const withFishjamVoIPAndroid = (config, props) => {
|
|
198
|
+
if (!props?.android?.enableVoIP) {
|
|
199
|
+
return config;
|
|
200
|
+
}
|
|
201
|
+
const fallbackService = resolveFallbackMessagingService(props);
|
|
202
|
+
return (0, config_plugins_1.withAndroidManifest)(config, (configuration) => {
|
|
203
|
+
applyVoIPManifest(configuration.modResults, props, fallbackService);
|
|
204
|
+
return configuration;
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
exports.withFishjamVoIPAndroid = withFishjamVoIPAndroid;
|