@capgo/capacitor-stream-call 0.0.50 → 0.0.56
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/CapgoCapacitorStreamCall.podspec +2 -2
- package/Package.swift +1 -1
- package/README.md +24 -14
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +8 -2
- package/dist/docs.json +39 -0
- package/dist/esm/definitions.d.ts +7 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +63 -12
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +54 -1
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
14
|
s.ios.deployment_target = '14.0'
|
|
15
15
|
s.dependency 'Capacitor'
|
|
16
|
-
s.dependency 'StreamVideo', '1.
|
|
17
|
-
s.dependency 'StreamVideoSwiftUI', '1.
|
|
16
|
+
s.dependency 'StreamVideo', '1.24.0'
|
|
17
|
+
s.dependency 'StreamVideoSwiftUI', '1.24.0'
|
|
18
18
|
s.swift_version = '5.1'
|
|
19
19
|
end
|
package/Package.swift
CHANGED
|
@@ -11,7 +11,7 @@ let package = Package(
|
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
13
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0"),
|
|
14
|
-
.package(url: "https://github.com/GetStream/stream-video-swift.git", exact: "1.
|
|
14
|
+
.package(url: "https://github.com/GetStream/stream-video-swift.git", exact: "1.24.0")
|
|
15
15
|
],
|
|
16
16
|
targets: [
|
|
17
17
|
.target(
|
package/README.md
CHANGED
|
@@ -518,24 +518,34 @@ Get detailed information about an active call including caller details
|
|
|
518
518
|
|
|
519
519
|
#### LoginOptions
|
|
520
520
|
|
|
521
|
-
| Prop
|
|
522
|
-
|
|
|
523
|
-
| **`token`**
|
|
524
|
-
| **`userId`**
|
|
525
|
-
| **`name`**
|
|
526
|
-
| **`imageURL`**
|
|
527
|
-
| **`apiKey`**
|
|
528
|
-
| **`magicDivId`**
|
|
521
|
+
| Prop | Type | Description |
|
|
522
|
+
| ----------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
523
|
+
| **`token`** | <code>string</code> | Stream Video API token |
|
|
524
|
+
| **`userId`** | <code>string</code> | User ID for the current user |
|
|
525
|
+
| **`name`** | <code>string</code> | Display name for the current user |
|
|
526
|
+
| **`imageURL`** | <code>string</code> | Optional avatar URL for the current user |
|
|
527
|
+
| **`apiKey`** | <code>string</code> | Stream Video API key |
|
|
528
|
+
| **`magicDivId`** | <code>string</code> | ID of the HTML element where the video will be rendered |
|
|
529
|
+
| **`pushNotificationsConfig`** | <code><a href="#pushnotificationsconfig">PushNotificationsConfig</a></code> | |
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
#### PushNotificationsConfig
|
|
533
|
+
|
|
534
|
+
| Prop | Type |
|
|
535
|
+
| ---------------------- | ------------------- |
|
|
536
|
+
| **`pushProviderName`** | <code>string</code> |
|
|
537
|
+
| **`voipProviderName`** | <code>string</code> |
|
|
529
538
|
|
|
530
539
|
|
|
531
540
|
#### CallOptions
|
|
532
541
|
|
|
533
|
-
| Prop | Type | Description
|
|
534
|
-
| ------------- | --------------------------------------------- |
|
|
535
|
-
| **`userIds`** | <code>string[]</code> | User ID of the person to call
|
|
536
|
-
| **`type`** | <code><a href="#calltype">CallType</a></code> | Type of call, defaults to 'default'
|
|
537
|
-
| **`ring`** | <code>boolean</code> | Whether to ring the other user, defaults to true
|
|
538
|
-
| **`team`** | <code>string</code> | Team name to call
|
|
542
|
+
| Prop | Type | Description |
|
|
543
|
+
| ------------- | --------------------------------------------- | --------------------------------------------------------------- |
|
|
544
|
+
| **`userIds`** | <code>string[]</code> | User ID of the person to call |
|
|
545
|
+
| **`type`** | <code><a href="#calltype">CallType</a></code> | Type of call, defaults to 'default' |
|
|
546
|
+
| **`ring`** | <code>boolean</code> | Whether to ring the other user, defaults to true |
|
|
547
|
+
| **`team`** | <code>string</code> | Team name to call |
|
|
548
|
+
| **`video`** | <code>boolean</code> | Whether to start the call with video enabled, defaults to false |
|
|
539
549
|
|
|
540
550
|
|
|
541
551
|
#### CallEvent
|
|
@@ -238,6 +238,10 @@ public class StreamCallPlugin : Plugin() {
|
|
|
238
238
|
|
|
239
239
|
// Notify WebView/JS about incoming call so it can render its own UI
|
|
240
240
|
notifyListeners("incomingCall", payload, true)
|
|
241
|
+
|
|
242
|
+
// Delay bringing app to foreground to allow the event to be processed first
|
|
243
|
+
kotlinx.coroutines.delay(500) // 500ms delay
|
|
244
|
+
bringAppToForeground()
|
|
241
245
|
} catch (e: Exception) {
|
|
242
246
|
android.util.Log.e("StreamCallPlugin", "Error getting call info for incoming call", e)
|
|
243
247
|
// Fallback to basic payload without caller info
|
|
@@ -246,10 +250,12 @@ public class StreamCallPlugin : Plugin() {
|
|
|
246
250
|
put("type", "incoming")
|
|
247
251
|
}
|
|
248
252
|
notifyListeners("incomingCall", payload, true)
|
|
253
|
+
|
|
254
|
+
// Delay bringing app to foreground to allow the event to be processed first
|
|
255
|
+
kotlinx.coroutines.delay(500) // 500ms delay
|
|
256
|
+
bringAppToForeground()
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
|
-
|
|
252
|
-
bringAppToForeground()
|
|
253
259
|
} else {
|
|
254
260
|
android.util.Log.w("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is null. Cannot process.")
|
|
255
261
|
}
|
package/dist/docs.json
CHANGED
|
@@ -546,6 +546,38 @@
|
|
|
546
546
|
"docs": "ID of the HTML element where the video will be rendered",
|
|
547
547
|
"complexTypes": [],
|
|
548
548
|
"type": "string | undefined"
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"name": "pushNotificationsConfig",
|
|
552
|
+
"tags": [],
|
|
553
|
+
"docs": "",
|
|
554
|
+
"complexTypes": [
|
|
555
|
+
"PushNotificationsConfig"
|
|
556
|
+
],
|
|
557
|
+
"type": "PushNotificationsConfig"
|
|
558
|
+
}
|
|
559
|
+
]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"name": "PushNotificationsConfig",
|
|
563
|
+
"slug": "pushnotificationsconfig",
|
|
564
|
+
"docs": "",
|
|
565
|
+
"tags": [],
|
|
566
|
+
"methods": [],
|
|
567
|
+
"properties": [
|
|
568
|
+
{
|
|
569
|
+
"name": "pushProviderName",
|
|
570
|
+
"tags": [],
|
|
571
|
+
"docs": "",
|
|
572
|
+
"complexTypes": [],
|
|
573
|
+
"type": "string"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"name": "voipProviderName",
|
|
577
|
+
"tags": [],
|
|
578
|
+
"docs": "",
|
|
579
|
+
"complexTypes": [],
|
|
580
|
+
"type": "string"
|
|
549
581
|
}
|
|
550
582
|
]
|
|
551
583
|
},
|
|
@@ -610,6 +642,13 @@
|
|
|
610
642
|
"docs": "Team name to call",
|
|
611
643
|
"complexTypes": [],
|
|
612
644
|
"type": "string | undefined"
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
"name": "video",
|
|
648
|
+
"tags": [],
|
|
649
|
+
"docs": "Whether to start the call with video enabled, defaults to false",
|
|
650
|
+
"complexTypes": [],
|
|
651
|
+
"type": "boolean | undefined"
|
|
613
652
|
}
|
|
614
653
|
]
|
|
615
654
|
},
|
|
@@ -21,6 +21,11 @@ export interface LoginOptions {
|
|
|
21
21
|
apiKey: string;
|
|
22
22
|
/** ID of the HTML element where the video will be rendered */
|
|
23
23
|
magicDivId?: string;
|
|
24
|
+
pushNotificationsConfig?: PushNotificationsConfig;
|
|
25
|
+
}
|
|
26
|
+
export interface PushNotificationsConfig {
|
|
27
|
+
pushProviderName: string;
|
|
28
|
+
voipProviderName: string;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* @typedef CallState
|
|
@@ -107,6 +112,8 @@ export interface CallOptions {
|
|
|
107
112
|
ring?: boolean;
|
|
108
113
|
/** Team name to call */
|
|
109
114
|
team?: string;
|
|
115
|
+
/** Whether to start the call with video enabled, defaults to false */
|
|
116
|
+
video?: boolean;
|
|
110
117
|
}
|
|
111
118
|
/**
|
|
112
119
|
* @interface StreamCallPlugin
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface LoginOptions\n * @description Configuration options for logging into the Stream Video service\n * @property {string} token - Stream Video API token for authentication\n * @property {string} userId - Unique identifier for the current user\n * @property {string} name - Display name for the current user\n * @property {string} [imageURL] - Avatar URL for the current user\n * @property {string} apiKey - Stream Video API key for your application\n * @property {string} [magicDivId] - DOM element ID where video will be rendered\n */\nexport interface LoginOptions {\n /** Stream Video API token */\n token: string;\n /** User ID for the current user */\n userId: string;\n /** Display name for the current user */\n name: string;\n /** Optional avatar URL for the current user */\n imageURL?: string;\n /** Stream Video API key */\n apiKey: string;\n /** ID of the HTML element where the video will be rendered */\n magicDivId?: string;\n}\n\n/**\n * @typedef CallState\n * @description Represents all possible call states from API and UI\n */\nexport type CallState =\n // User-facing states\n | 'idle'\n | 'ringing'\n | 'joining'\n | 'reconnecting'\n | 'joined'\n | 'leaving'\n | 'left'\n // Event-specific states\n | 'created'\n | 'session_started'\n | 'rejected'\n | 'missed'\n | 'accepted'\n | 'ended'\n | 'unknown';\n\n/**\n * @typedef CallType\n * @description Represents the pre-defined types of a call.\n * - `default`: Simple 1-1 or group video calling with sensible defaults. Video/audio enabled, backstage disabled. Admins/hosts have elevated permissions.\n * - `audio_room`: For audio-only spaces (like Clubhouse). Backstage enabled (requires `goLive`), pre-configured permissions for requesting to speak.\n * - `livestream`: For one-to-many streaming. Backstage enabled (requires `goLive`), access granted to all authenticated users.\n * - `development`: For testing ONLY. All permissions enabled, backstage disabled. **Not recommended for production.**\n */\nexport type CallType = 'default' | 'audio_room' | 'livestream' | 'development';\n\n/**\n * @interface CallMember\n * @description Information about a call member/participant\n * @property {string} userId - User ID of the member\n * @property {string} [name] - Display name of the user\n * @property {string} [imageURL] - Profile image URL of the user\n * @property {string} [role] - Role of the user in the call\n */\nexport interface CallMember {\n /** User ID of the member */\n userId: string;\n /** Display name of the user */\n name?: string;\n /** Profile image URL of the user */\n imageURL?: string;\n /** Role of the user in the call */\n role?: string;\n}\n\n/**\n * @interface CallEvent\n * @description Event emitted when call state changes\n * @property {string} callId - Unique identifier of the call\n * @property {CallState} state - Current state of the call\n * @property {string} [userId] - User ID of the participant who triggered the event\n * @property {string} [reason] - Reason for the call state change\n * @property {CallMember} [caller] - Information about the caller (for incoming calls)\n * @property {CallMember[]} [members] - List of call members\n */\nexport interface CallEvent {\n /** ID of the call */\n callId: string;\n /** Current state of the call */\n state: CallState;\n /** User ID of the participant in the call who triggered the event */\n userId?: string;\n /** Reason for the call state change, if applicable */\n reason?: string;\n /** Information about the caller (for incoming calls) */\n caller?: CallMember;\n /** List of call members */\n members?: CallMember[];\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface SuccessResponse\n * @description Standard response indicating operation success/failure\n * @property {boolean} success - Whether the operation succeeded\n */\nexport interface SuccessResponse {\n /** Whether the operation was successful */\n success: boolean;\n}\n\n/**\n * @interface CallOptions\n * @description Options for initiating a video call\n * @property {string[]} userIds - IDs of the users to call\n * @property {CallType} [type=default] - Type of call\n * @property {boolean} [ring=true] - Whether to send ring notification\n * @property {string} [team] - Team name to call\n */\nexport interface CallOptions {\n /** User ID of the person to call */\n userIds: string[];\n /** Type of call, defaults to 'default' */\n type?: CallType;\n /** Whether to ring the other user, defaults to true */\n ring?: boolean;\n /** Team name to call */\n team?: string;\n}\n\n/**\n * @interface StreamCallPlugin\n * @description Capacitor plugin for Stream Video calling functionality\n */\nexport interface StreamCallPlugin {\n /**\n * Login to Stream Video service\n * @param {LoginOptions} options - Login configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.login({\n * token: 'your-token',\n * userId: 'user-123',\n * name: 'John Doe',\n * apiKey: 'your-api-key'\n * });\n */\n login(options: LoginOptions): Promise<SuccessResponse>;\n\n /**\n * Logout from Stream Video service\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.logout();\n */\n logout(): Promise<SuccessResponse>;\n\n /**\n * Initiate a call to another user\n * @param {CallOptions} options - Call configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.call({\n * userId: 'user-456',\n * type: 'video',\n * ring: true\n * });\n */\n call(options: CallOptions): Promise<SuccessResponse>;\n\n /**\n * End the current call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.endCall();\n */\n endCall(): Promise<SuccessResponse>;\n\n /**\n * Enable or disable microphone\n * @param {{ enabled: boolean }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setMicrophoneEnabled({ enabled: false });\n */\n setMicrophoneEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Enable or disable camera\n * @param {{ enabled: boolean }} options - Camera state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setCameraEnabled({ enabled: false });\n */\n setCameraEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Add listener for call events\n * @param {'callEvent'} eventName - Name of the event to listen for\n * @param {(event: CallEvent) => void} listenerFunc - Callback function\n * @returns {Promise<{ remove: () => Promise<void> }>} Function to remove listener\n * @example\n * const listener = await StreamCall.addListener('callEvent', (event) => {\n * console.log(`Call ${event.callId} is now ${event.state}`);\n * });\n */\n addListener(\n eventName: 'callEvent',\n listenerFunc: (event: CallEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Listen for lock-screen incoming call (Android only).\n * Fired when the app is shown by full-screen intent before user interaction.\n */\n addListener(\n eventName: 'incomingCall',\n listenerFunc: (event: IncomingCallPayload) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Remove all event listeners\n * @returns {Promise<void>}\n * @example\n * await StreamCall.removeAllListeners();\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Accept an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.acceptCall();\n */\n acceptCall(): Promise<SuccessResponse>;\n\n /**\n * Reject an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.rejectCall();\n */\n rejectCall(): Promise<SuccessResponse>;\n\n /**\n * Check if camera is enabled\n * @returns {Promise<CameraEnabledResponse>} Camera enabled status\n * @example\n * const isCameraEnabled = await StreamCall.isCameraEnabled();\n * console.log(isCameraEnabled);\n */\n isCameraEnabled(): Promise<CameraEnabledResponse>;\n\n /**\n * Get the current call status\n * @returns {Promise<CallEvent>} Current call status as a CallEvent\n * @example\n * const callStatus = await StreamCall.getCallStatus();\n * console.log(callStatus);\n */\n getCallStatus(): Promise<CallEvent>;\n\n /**\n * Set speakerphone on\n * @param {{ name: string }} options - Speakerphone name\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setSpeaker({ name: 'speaker' });\n */\n setSpeaker(options: { name: string }): Promise<SuccessResponse>;\n\n /**\n * Switch camera\n * @param {{ camera: 'front' | 'back' }} options - Camera to switch to\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.switchCamera({ camera: 'back' });\n */\n switchCamera(options: { camera: 'front' | 'back' }): Promise<SuccessResponse>;\n\n /**\n * Get detailed information about an active call including caller details\n * @param options - Options containing the call ID\n */\n getCallInfo(options: { callId: string }): Promise<CallEvent>;\n}\n\n/**\n * @interface IncomingCallPayload\n * @description Payload delivered with \"incomingCall\" event (Android lock-screen).\n * @property {string} cid - Call CID (type:id)\n * @property {string} type - Always \"incoming\" for this event\n * @property {CallMember} [caller] - Information about the caller\n */\nexport interface IncomingCallPayload {\n /** Full call CID (e.g. default:123) */\n cid: string;\n /** Event type (currently always \"incoming\") */\n type: 'incoming';\n /** Information about the caller */\n caller?: CallMember;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface LoginOptions\n * @description Configuration options for logging into the Stream Video service\n * @property {string} token - Stream Video API token for authentication\n * @property {string} userId - Unique identifier for the current user\n * @property {string} name - Display name for the current user\n * @property {string} [imageURL] - Avatar URL for the current user\n * @property {string} apiKey - Stream Video API key for your application\n * @property {string} [magicDivId] - DOM element ID where video will be rendered\n */\nexport interface LoginOptions {\n /** Stream Video API token */\n token: string;\n /** User ID for the current user */\n userId: string;\n /** Display name for the current user */\n name: string;\n /** Optional avatar URL for the current user */\n imageURL?: string;\n /** Stream Video API key */\n apiKey: string;\n /** ID of the HTML element where the video will be rendered */\n magicDivId?: string;\n pushNotificationsConfig?: PushNotificationsConfig;\n}\n\nexport interface PushNotificationsConfig {\n pushProviderName: string;\n voipProviderName: string;\n}\n\n/**\n * @typedef CallState\n * @description Represents all possible call states from API and UI\n */\nexport type CallState =\n // User-facing states\n | 'idle'\n | 'ringing'\n | 'joining'\n | 'reconnecting'\n | 'joined'\n | 'leaving'\n | 'left'\n // Event-specific states\n | 'created'\n | 'session_started'\n | 'rejected'\n | 'missed'\n | 'accepted'\n | 'ended'\n | 'unknown';\n\n/**\n * @typedef CallType\n * @description Represents the pre-defined types of a call.\n * - `default`: Simple 1-1 or group video calling with sensible defaults. Video/audio enabled, backstage disabled. Admins/hosts have elevated permissions.\n * - `audio_room`: For audio-only spaces (like Clubhouse). Backstage enabled (requires `goLive`), pre-configured permissions for requesting to speak.\n * - `livestream`: For one-to-many streaming. Backstage enabled (requires `goLive`), access granted to all authenticated users.\n * - `development`: For testing ONLY. All permissions enabled, backstage disabled. **Not recommended for production.**\n */\nexport type CallType = 'default' | 'audio_room' | 'livestream' | 'development';\n\n/**\n * @interface CallMember\n * @description Information about a call member/participant\n * @property {string} userId - User ID of the member\n * @property {string} [name] - Display name of the user\n * @property {string} [imageURL] - Profile image URL of the user\n * @property {string} [role] - Role of the user in the call\n */\nexport interface CallMember {\n /** User ID of the member */\n userId: string;\n /** Display name of the user */\n name?: string;\n /** Profile image URL of the user */\n imageURL?: string;\n /** Role of the user in the call */\n role?: string;\n}\n\n/**\n * @interface CallEvent\n * @description Event emitted when call state changes\n * @property {string} callId - Unique identifier of the call\n * @property {CallState} state - Current state of the call\n * @property {string} [userId] - User ID of the participant who triggered the event\n * @property {string} [reason] - Reason for the call state change\n * @property {CallMember} [caller] - Information about the caller (for incoming calls)\n * @property {CallMember[]} [members] - List of call members\n */\nexport interface CallEvent {\n /** ID of the call */\n callId: string;\n /** Current state of the call */\n state: CallState;\n /** User ID of the participant in the call who triggered the event */\n userId?: string;\n /** Reason for the call state change, if applicable */\n reason?: string;\n /** Information about the caller (for incoming calls) */\n caller?: CallMember;\n /** List of call members */\n members?: CallMember[];\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface SuccessResponse\n * @description Standard response indicating operation success/failure\n * @property {boolean} success - Whether the operation succeeded\n */\nexport interface SuccessResponse {\n /** Whether the operation was successful */\n success: boolean;\n}\n\n/**\n * @interface CallOptions\n * @description Options for initiating a video call\n * @property {string[]} userIds - IDs of the users to call\n * @property {CallType} [type=default] - Type of call\n * @property {boolean} [ring=true] - Whether to send ring notification\n * @property {string} [team] - Team name to call\n */\nexport interface CallOptions {\n /** User ID of the person to call */\n userIds: string[];\n /** Type of call, defaults to 'default' */\n type?: CallType;\n /** Whether to ring the other user, defaults to true */\n ring?: boolean;\n /** Team name to call */\n team?: string;\n /** Whether to start the call with video enabled, defaults to false */\n video?: boolean;\n}\n\n/**\n * @interface StreamCallPlugin\n * @description Capacitor plugin for Stream Video calling functionality\n */\nexport interface StreamCallPlugin {\n /**\n * Login to Stream Video service\n * @param {LoginOptions} options - Login configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.login({\n * token: 'your-token',\n * userId: 'user-123',\n * name: 'John Doe',\n * apiKey: 'your-api-key'\n * });\n */\n login(options: LoginOptions): Promise<SuccessResponse>;\n\n /**\n * Logout from Stream Video service\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.logout();\n */\n logout(): Promise<SuccessResponse>;\n\n /**\n * Initiate a call to another user\n * @param {CallOptions} options - Call configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.call({\n * userId: 'user-456',\n * type: 'video',\n * ring: true\n * });\n */\n call(options: CallOptions): Promise<SuccessResponse>;\n\n /**\n * End the current call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.endCall();\n */\n endCall(): Promise<SuccessResponse>;\n\n /**\n * Enable or disable microphone\n * @param {{ enabled: boolean }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setMicrophoneEnabled({ enabled: false });\n */\n setMicrophoneEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Enable or disable camera\n * @param {{ enabled: boolean }} options - Camera state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setCameraEnabled({ enabled: false });\n */\n setCameraEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Add listener for call events\n * @param {'callEvent'} eventName - Name of the event to listen for\n * @param {(event: CallEvent) => void} listenerFunc - Callback function\n * @returns {Promise<{ remove: () => Promise<void> }>} Function to remove listener\n * @example\n * const listener = await StreamCall.addListener('callEvent', (event) => {\n * console.log(`Call ${event.callId} is now ${event.state}`);\n * });\n */\n addListener(\n eventName: 'callEvent',\n listenerFunc: (event: CallEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Listen for lock-screen incoming call (Android only).\n * Fired when the app is shown by full-screen intent before user interaction.\n */\n addListener(\n eventName: 'incomingCall',\n listenerFunc: (event: IncomingCallPayload) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Remove all event listeners\n * @returns {Promise<void>}\n * @example\n * await StreamCall.removeAllListeners();\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Accept an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.acceptCall();\n */\n acceptCall(): Promise<SuccessResponse>;\n\n /**\n * Reject an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.rejectCall();\n */\n rejectCall(): Promise<SuccessResponse>;\n\n /**\n * Check if camera is enabled\n * @returns {Promise<CameraEnabledResponse>} Camera enabled status\n * @example\n * const isCameraEnabled = await StreamCall.isCameraEnabled();\n * console.log(isCameraEnabled);\n */\n isCameraEnabled(): Promise<CameraEnabledResponse>;\n\n /**\n * Get the current call status\n * @returns {Promise<CallEvent>} Current call status as a CallEvent\n * @example\n * const callStatus = await StreamCall.getCallStatus();\n * console.log(callStatus);\n */\n getCallStatus(): Promise<CallEvent>;\n\n /**\n * Set speakerphone on\n * @param {{ name: string }} options - Speakerphone name\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setSpeaker({ name: 'speaker' });\n */\n setSpeaker(options: { name: string }): Promise<SuccessResponse>;\n\n /**\n * Switch camera\n * @param {{ camera: 'front' | 'back' }} options - Camera to switch to\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.switchCamera({ camera: 'back' });\n */\n switchCamera(options: { camera: 'front' | 'back' }): Promise<SuccessResponse>;\n\n /**\n * Get detailed information about an active call including caller details\n * @param options - Options containing the call ID\n */\n getCallInfo(options: { callId: string }): Promise<CallEvent>;\n}\n\n/**\n * @interface IncomingCallPayload\n * @description Payload delivered with \"incomingCall\" event (Android lock-screen).\n * @property {string} cid - Call CID (type:id)\n * @property {string} type - Always \"incoming\" for this event\n * @property {CallMember} [caller] - Information about the caller\n */\nexport interface IncomingCallPayload {\n /** Full call CID (e.g. default:123) */\n cid: string;\n /** Event type (currently always \"incoming\") */\n type: 'incoming';\n /** Information about the caller */\n caller?: CallMember;\n}\n"]}
|
|
@@ -45,9 +45,12 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
45
45
|
private var tokenSubscription: AnyCancellable?
|
|
46
46
|
private var activeCallSubscription: AnyCancellable?
|
|
47
47
|
private var lastVoIPToken: String?
|
|
48
|
+
private var touchInterceptView: TouchInterceptView?
|
|
48
49
|
|
|
49
50
|
private var streamVideo: StreamVideo?
|
|
50
51
|
|
|
52
|
+
private var pushNotificationsConfig: PushNotificationsConfig?
|
|
53
|
+
|
|
51
54
|
// Store current call info for getCallStatus
|
|
52
55
|
private var currentCallId: String = ""
|
|
53
56
|
private var currentCallState: String = ""
|
|
@@ -230,6 +233,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
230
233
|
print("- In call state detected")
|
|
231
234
|
print("- All participants: \(String(describing: viewModel.participants))")
|
|
232
235
|
|
|
236
|
+
// Enable touch interceptor when call becomes active
|
|
237
|
+
self.touchInterceptView?.setCallActive(true)
|
|
238
|
+
|
|
233
239
|
// Create/update overlay and make visible when there's an active call
|
|
234
240
|
self.createCallOverlayView()
|
|
235
241
|
|
|
@@ -283,17 +289,18 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
283
289
|
self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing", caller: caller, members: members)
|
|
284
290
|
}
|
|
285
291
|
} else if newState == .idle {
|
|
286
|
-
|
|
287
|
-
let endingCallId = viewModel.call?.cId
|
|
288
|
-
print("Call state changed to idle. EndingCallId: \(String(describing: endingCallId)), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
|
|
292
|
+
print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
|
|
289
293
|
|
|
290
|
-
//
|
|
294
|
+
// Disable touch interceptor when call becomes inactive
|
|
295
|
+
self.touchInterceptView?.setCallActive(false)
|
|
296
|
+
|
|
297
|
+
// Only notify about call ending if we have a valid stored call ID and there's truly no active call
|
|
291
298
|
// This prevents false "left" events during normal state transitions
|
|
292
|
-
if
|
|
293
|
-
print("Call actually ending: \(
|
|
299
|
+
if !self.currentCallId.isEmpty && self.streamVideo?.state.activeCall == nil {
|
|
300
|
+
print("Call actually ending: \(self.currentCallId)")
|
|
294
301
|
|
|
295
|
-
// Notify that call has ended - use the
|
|
296
|
-
self.updateCallStatusAndNotify(callId:
|
|
302
|
+
// Notify that call has ended - use the stored call ID
|
|
303
|
+
self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
|
|
297
304
|
|
|
298
305
|
// Reset notification flag when call ends
|
|
299
306
|
self.hasNotifiedCallJoined = false
|
|
@@ -301,7 +308,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
301
308
|
// Remove the call overlay view when not in a call
|
|
302
309
|
self.ensureViewRemoved()
|
|
303
310
|
} else {
|
|
304
|
-
print("Not sending left event -
|
|
311
|
+
print("Not sending left event - CurrentCallId: \(self.currentCallId), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
|
|
305
312
|
}
|
|
306
313
|
}
|
|
307
314
|
} catch {
|
|
@@ -355,6 +362,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
355
362
|
customData: [:]
|
|
356
363
|
)
|
|
357
364
|
|
|
365
|
+
// Get push notifications config if provided
|
|
366
|
+
if let pushConfig = call.getObject("pushNotificationsConfig") {
|
|
367
|
+
let pushProviderName = pushConfig["pushProviderName"] as? String ?? "ios-apn"
|
|
368
|
+
let voipProviderName = pushConfig["voipProviderName"] as? String ?? "ios-voip"
|
|
369
|
+
|
|
370
|
+
self.pushNotificationsConfig = PushNotificationsConfig(
|
|
371
|
+
pushProviderInfo: PushProviderInfo(name: pushProviderName, pushProvider: .apn),
|
|
372
|
+
voipPushProviderInfo: PushProviderInfo(name: voipProviderName, pushProvider: .apn)
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
358
376
|
let credentials = UserCredentials(user: user, tokenValue: token)
|
|
359
377
|
SecureUserRepository.shared.save(user: credentials)
|
|
360
378
|
// Initialize Stream Video with new credentials
|
|
@@ -403,6 +421,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
403
421
|
Task { @MainActor in
|
|
404
422
|
// self.overlayViewModel?.updateCall(nil)
|
|
405
423
|
// self.overlayViewModel?.updateStreamVideo(nil)
|
|
424
|
+
self.touchInterceptView?.setCallActive(false)
|
|
406
425
|
self.overlayView?.isHidden = true
|
|
407
426
|
self.webView?.isOpaque = true
|
|
408
427
|
}
|
|
@@ -454,6 +473,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
454
473
|
let callType = call.getString("type") ?? "default"
|
|
455
474
|
let shouldRing = call.getBool("ring") ?? true
|
|
456
475
|
let team = call.getString("team")
|
|
476
|
+
let video = call.getBool("video") ?? false
|
|
457
477
|
|
|
458
478
|
// Generate a unique call ID
|
|
459
479
|
let callId = UUID().uuidString
|
|
@@ -465,14 +485,13 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
465
485
|
print("- Call Type: \(callType)")
|
|
466
486
|
print("- Users: \(members)")
|
|
467
487
|
print("- Should Ring: \(shouldRing)")
|
|
468
|
-
print("- Team: \(team)")
|
|
488
|
+
print("- Team: \(String(describing: team))")
|
|
469
489
|
|
|
470
490
|
// Create the call object
|
|
471
491
|
await self.callViewModel?.startCall(
|
|
472
492
|
callType: callType,
|
|
473
493
|
callId: callId,
|
|
474
|
-
members: members.map { Member(userId: $0, role: nil, customData: [:], updatedAt: nil) },
|
|
475
|
-
ring: shouldRing
|
|
494
|
+
members: members.map { Member(userId: $0, role: nil, customData: [:], updatedAt: nil) }, team: team, ring: shouldRing, video: video
|
|
476
495
|
)
|
|
477
496
|
|
|
478
497
|
// Wait for call state to be populated by WebSocket events
|
|
@@ -585,6 +604,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
585
604
|
}
|
|
586
605
|
|
|
587
606
|
await MainActor.run {
|
|
607
|
+
self.touchInterceptView?.setCallActive(false)
|
|
588
608
|
self.overlayView?.isHidden = true
|
|
589
609
|
self.webView?.isOpaque = true
|
|
590
610
|
}
|
|
@@ -613,6 +633,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
613
633
|
await callViewModel?.hangUp()
|
|
614
634
|
|
|
615
635
|
await MainActor.run {
|
|
636
|
+
self.touchInterceptView?.setCallActive(false)
|
|
616
637
|
self.overlayView?.isHidden = true
|
|
617
638
|
self.webView?.isOpaque = true
|
|
618
639
|
}
|
|
@@ -771,6 +792,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
771
792
|
apiKey: apiKey,
|
|
772
793
|
user: savedCredentials.user,
|
|
773
794
|
token: UserToken(stringLiteral: savedCredentials.tokenValue),
|
|
795
|
+
pushNotificationsConfig: self.pushNotificationsConfig ?? .default,
|
|
774
796
|
tokenProvider: {completion in
|
|
775
797
|
guard let savedCredentials = SecureUserRepository.shared.loadCurrentUser() else {
|
|
776
798
|
print("No saved credentials or API key found, cannot refresh token")
|
|
@@ -850,6 +872,14 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
850
872
|
parent.insertSubview(touchInterceptView, aboveSubview: webView)
|
|
851
873
|
}
|
|
852
874
|
|
|
875
|
+
// Set up active call check function
|
|
876
|
+
touchInterceptView.setActiveCallCheck { [weak self] in
|
|
877
|
+
return self?.streamVideo?.state.activeCall != nil
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Store reference to touch intercept view
|
|
881
|
+
self.touchInterceptView = touchInterceptView
|
|
882
|
+
|
|
853
883
|
// Setup constraints for touchInterceptView to cover the entire parent
|
|
854
884
|
NSLayoutConstraint.activate([
|
|
855
885
|
touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
|
|
@@ -907,6 +937,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
907
937
|
// Ensure touch intercept view is on top
|
|
908
938
|
if let touchInterceptView = parent.subviews.first(where: { $0 is TouchInterceptView }) {
|
|
909
939
|
parent.bringSubviewToFront(touchInterceptView)
|
|
940
|
+
// Update reference and set call active
|
|
941
|
+
self.touchInterceptView = touchInterceptView as? TouchInterceptView
|
|
942
|
+
|
|
943
|
+
// Set up active call check function
|
|
944
|
+
self.touchInterceptView?.setActiveCallCheck { [weak self] in
|
|
945
|
+
return self?.streamVideo?.state.activeCall != nil
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
self.touchInterceptView?.setCallActive(true)
|
|
910
949
|
} else {
|
|
911
950
|
// Create touch intercept view if not already created
|
|
912
951
|
let touchInterceptView = TouchInterceptView(frame: parent.bounds)
|
|
@@ -916,6 +955,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
916
955
|
touchInterceptView.setupWithWebView(webView, overlayView: overlayView.view)
|
|
917
956
|
parent.addSubview(touchInterceptView)
|
|
918
957
|
|
|
958
|
+
// Set up active call check function
|
|
959
|
+
touchInterceptView.setActiveCallCheck { [weak self] in
|
|
960
|
+
return self?.streamVideo?.state.activeCall != nil
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Store reference and set call active
|
|
964
|
+
self.touchInterceptView = touchInterceptView
|
|
965
|
+
self.touchInterceptView?.setCallActive(true)
|
|
966
|
+
|
|
919
967
|
NSLayoutConstraint.activate([
|
|
920
968
|
touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
|
|
921
969
|
touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
|
@@ -926,6 +974,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
926
974
|
}
|
|
927
975
|
|
|
928
976
|
private func ensureViewRemoved() {
|
|
977
|
+
// Disable touch interceptor when overlay is removed
|
|
978
|
+
self.touchInterceptView?.setCallActive(false)
|
|
979
|
+
|
|
929
980
|
// Check if we have an overlay view
|
|
930
981
|
if let existingOverlayView = self.overlayView {
|
|
931
982
|
print("Hiding call overlay view")
|
|
@@ -9,6 +9,8 @@ class TouchInterceptView: UIView {
|
|
|
9
9
|
private var lastTouchPoint: CGPoint?
|
|
10
10
|
private let touchThreshold: CGFloat = 5.0 // pixels
|
|
11
11
|
private let timerDelay: TimeInterval = 0.1 // seconds
|
|
12
|
+
private var isCallActive: Bool = false
|
|
13
|
+
private var hasActiveCallCheck: (() -> Bool)?
|
|
12
14
|
|
|
13
15
|
func setupWithWebView(_ webView: UIView, overlayView: UIView) {
|
|
14
16
|
self.webView = webView
|
|
@@ -20,6 +22,34 @@ class TouchInterceptView: UIView {
|
|
|
20
22
|
os_log(.debug, "TouchInterceptView: setupWithWebView - webView: %{public}s, overlayView: %{public}s", String(describing: webView), String(describing: overlayView))
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
func setActiveCallCheck(_ check: @escaping () -> Bool) {
|
|
26
|
+
self.hasActiveCallCheck = check
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func setCallActive(_ active: Bool) {
|
|
30
|
+
self.isCallActive = active
|
|
31
|
+
os_log(.debug, "TouchInterceptView: setCallActive - %{public}s", String(describing: active))
|
|
32
|
+
|
|
33
|
+
// Cancel any pending timer when call becomes inactive
|
|
34
|
+
if !active {
|
|
35
|
+
forwardTimer?.invalidate()
|
|
36
|
+
forwardTimer = nil
|
|
37
|
+
lastTouchPoint = nil
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private func shouldInterceptTouches() -> Bool {
|
|
42
|
+
// Check both our flag and actual call state
|
|
43
|
+
let hasActiveCall = hasActiveCallCheck?() ?? false
|
|
44
|
+
let shouldIntercept = isCallActive && hasActiveCall
|
|
45
|
+
|
|
46
|
+
if isCallActive != hasActiveCall {
|
|
47
|
+
os_log(.debug, "TouchInterceptView: State mismatch - isCallActive: %{public}s, hasActiveCall: %{public}s", String(describing: isCallActive), String(describing: hasActiveCall))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return shouldIntercept
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
private func isInteractive(_ view: UIView) -> Bool {
|
|
24
54
|
if view is UIControl { return true }
|
|
25
55
|
if let grs = view.gestureRecognizers, !grs.isEmpty { return true }
|
|
@@ -80,7 +110,19 @@ class TouchInterceptView: UIView {
|
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
83
|
-
|
|
113
|
+
|
|
114
|
+
// Check if we should intercept touches
|
|
115
|
+
if !shouldInterceptTouches() {
|
|
116
|
+
if let webView = self.webView {
|
|
117
|
+
let webPoint = self.convert(point, to: webView)
|
|
118
|
+
let result = webView.hitTest(webPoint, with: event)
|
|
119
|
+
os_log(.debug, "TouchInterceptView: hitTest - Not intercepting, direct WebView result %{public}s at %{public}s", String(describing: result), String(describing: webPoint))
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
os_log(.debug, "TouchInterceptView: hitTest entry at %{public}s, callActive: %{public}s", String(describing: point), String(describing: isCallActive))
|
|
84
126
|
|
|
85
127
|
// Check if this is same touch location continuing
|
|
86
128
|
if let lastPoint = lastTouchPoint {
|
|
@@ -120,6 +162,17 @@ class TouchInterceptView: UIView {
|
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
165
|
+
// Check if we should intercept touches
|
|
166
|
+
if !shouldInterceptTouches() {
|
|
167
|
+
guard let webView = self.webView else {
|
|
168
|
+
return super.point(inside: point, with: event)
|
|
169
|
+
}
|
|
170
|
+
let webViewPoint = self.convert(point, to: webView)
|
|
171
|
+
let result = webView.point(inside: webViewPoint, with: event)
|
|
172
|
+
os_log(.debug, "TouchInterceptView: point(inside) - Not intercepting, WebView only (%{public}s at %{public}s) for original point %{public}s = %s", String(describing: result), String(describing: webViewPoint), String(describing: point), String(describing: result))
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
|
|
123
176
|
guard let webView = self.webView else {
|
|
124
177
|
os_log(.debug, "TouchInterceptView: point(inside) - webView is nil for point %{public}s. Checking overlay or deferring to super.", String(describing: point))
|
|
125
178
|
if let overlayView = self.overlayView, !overlayView.isHidden {
|
package/package.json
CHANGED