@capgo/capacitor-stream-call 7.4.0 → 7.7.7

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 CHANGED
@@ -2,12 +2,16 @@
2
2
  <a href="https://capgo.app/"><img src='https://raw.githubusercontent.com/Cap-go/capgo/main/assets/capgo_banner.png' alt='Capgo - Instant updates for capacitor'/></a>
3
3
 
4
4
  <div align="center">
5
- <h2><a href="https://capgo.app/?ref=plugin"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
- <h2><a href="https://capgo.app/consulting/?ref=plugin"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
5
+ <h2><a href="https://capgo.app/?ref=plugin_streamcall"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
+ <h2><a href="https://capgo.app/consulting/?ref=plugin_streamcall"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
7
7
  </div>
8
8
 
9
9
  A Capacitor plugin that uses the [Stream Video SDK](https://getstream.io/) to enable video calling functionality in your app.
10
10
 
11
+ ## Documentation
12
+
13
+ The most complete doc is available here: https://capgo.app/docs/plugins/streamcall/
14
+
11
15
  ## Installation
12
16
 
13
17
  ```bash
@@ -259,6 +263,7 @@ export class CallService {
259
263
  * [`setDynamicStreamVideoApikey(...)`](#setdynamicstreamvideoapikey)
260
264
  * [`getDynamicStreamVideoApikey()`](#getdynamicstreamvideoapikey)
261
265
  * [`getCurrentUser()`](#getcurrentuser)
266
+ * [`getPluginVersion()`](#getpluginversion)
262
267
  * [Interfaces](#interfaces)
263
268
  * [Type Aliases](#type-aliases)
264
269
  * [Enums](#enums)
@@ -612,6 +617,19 @@ Get the current user's information
612
617
  --------------------
613
618
 
614
619
 
620
+ ### getPluginVersion()
621
+
622
+ ```typescript
623
+ getPluginVersion() => Promise<{ version: string; }>
624
+ ```
625
+
626
+ Get the native Capacitor plugin version
627
+
628
+ **Returns:** <code>Promise&lt;{ version: string; }&gt;</code>
629
+
630
+ --------------------
631
+
632
+
615
633
  ### Interfaces
616
634
 
617
635
 
@@ -77,8 +77,8 @@ dependencies {
77
77
 
78
78
 
79
79
  // Stream dependencies
80
- implementation("io.getstream:stream-video-android-ui-compose:1.14.0")
81
- implementation("io.getstream:stream-video-android-core:1.14.0")
80
+ implementation("io.getstream:stream-video-android-ui-compose:1.16.0")
81
+ implementation("io.getstream:stream-video-android-core:1.16.0")
82
82
  implementation("io.getstream:stream-android-push:1.3.2")
83
83
  implementation("io.getstream:stream-android-push-firebase:1.3.2")
84
84
 
@@ -107,6 +107,7 @@ import kotlinx.coroutines.flow.collectLatest
107
107
  // It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
108
108
  @CapacitorPlugin(name = "StreamCall")
109
109
  class StreamCallPlugin : Plugin() {
110
+ private val pluginVersion = "7.7.4"
110
111
  private var streamVideoClient: StreamVideo? = null
111
112
  private var state: State = State.NOT_INITIALIZED
112
113
  private var overlayView: ComposeView? = null
@@ -265,6 +266,7 @@ class StreamCallPlugin : Plugin() {
265
266
  if (action === "io.getstream.video.android.action.INCOMING_CALL") {
266
267
  Log.d("StreamCallPlugin", "handleOnNewIntent: Matched INCOMING_CALL action")
267
268
  // We need to make sure the activity is visible on locked screen in such case
269
+ bringAppToForeground()
268
270
  changeActivityAsVisibleOnLockScreen(this@StreamCallPlugin.activity, true)
269
271
  activity?.runOnUiThread {
270
272
  val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
@@ -275,6 +277,12 @@ class StreamCallPlugin : Plugin() {
275
277
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
276
278
  Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Got call object: ${call?.id}")
277
279
 
280
+ val preview = JSObject().apply {
281
+ put("cid", cid.cid)
282
+ put("type", "incoming")
283
+ }
284
+ notifyListeners("incomingCall", preview, true)
285
+
278
286
  // Try to get caller information from the call
279
287
  kotlinx.coroutines.GlobalScope.launch {
280
288
  try {
@@ -301,9 +309,6 @@ class StreamCallPlugin : Plugin() {
301
309
 
302
310
  // Notify WebView/JS about incoming call so it can render its own UI
303
311
  notifyListeners("incomingCall", payload, true)
304
- // Delay bringing app to foreground to allow the event to be processed first
305
- kotlinx.coroutines.delay(500) // 500ms delay
306
- bringAppToForeground()
307
312
  } catch (e: Exception) {
308
313
  Log.e("StreamCallPlugin", "Error getting call info for incoming call", e)
309
314
  // Fallback to basic payload without caller info
@@ -313,9 +318,6 @@ class StreamCallPlugin : Plugin() {
313
318
  }
314
319
  notifyListeners("incomingCall", payload, true)
315
320
  ringingCallId = cid.cid;
316
- // Delay bringing app to foreground to allow the event to be processed first
317
- kotlinx.coroutines.delay(500) // 500ms delay
318
- bringAppToForeground()
319
321
  }
320
322
  }
321
323
  } else {
@@ -1370,8 +1372,6 @@ class StreamCallPlugin : Plugin() {
1370
1372
  updateCallStatusAndNotify(call.cid, "joined")
1371
1373
  CallUIController.layoutType.value = LayoutType.GRID
1372
1374
  ringingCallId = ""
1373
- // Make sure activity is visible on lock screen
1374
- changeActivityAsVisibleOnLockScreen(this@StreamCallPlugin.activity, true)
1375
1375
 
1376
1376
  cameraStatusJob?.cancel()
1377
1377
  microphoneStatusJob?.cancel()
@@ -3256,4 +3256,15 @@ class StreamCallPlugin : Plugin() {
3256
3256
  false
3257
3257
  }
3258
3258
  }
3259
+
3260
+ @PluginMethod
3261
+ fun getPluginVersion(call: PluginCall) {
3262
+ try {
3263
+ val ret = JSObject()
3264
+ ret.put("version", pluginVersion)
3265
+ call.resolve(ret)
3266
+ } catch (e: Exception) {
3267
+ call.reject("Could not get plugin version", e)
3268
+ }
3269
+ }
3259
3270
  }
package/dist/docs.json CHANGED
@@ -598,6 +598,25 @@
598
598
  "CurrentUserResponse"
599
599
  ],
600
600
  "slug": "getcurrentuser"
601
+ },
602
+ {
603
+ "name": "getPluginVersion",
604
+ "signature": "() => Promise<{ version: string; }>",
605
+ "parameters": [],
606
+ "returns": "Promise<{ version: string; }>",
607
+ "tags": [
608
+ {
609
+ "name": "returns",
610
+ "text": "an Promise with version for this device"
611
+ },
612
+ {
613
+ "name": "throws",
614
+ "text": "An error if the something went wrong"
615
+ }
616
+ ],
617
+ "docs": "Get the native Capacitor plugin version",
618
+ "complexTypes": [],
619
+ "slug": "getpluginversion"
601
620
  }
602
621
  ],
603
622
  "properties": []
@@ -369,6 +369,15 @@ export interface StreamCallPlugin {
369
369
  * console.log(currentUser);
370
370
  */
371
371
  getCurrentUser(): Promise<CurrentUserResponse>;
372
+ /**
373
+ * Get the native Capacitor plugin version
374
+ *
375
+ * @returns {Promise<{ id: string }>} an Promise with version for this device
376
+ * @throws An error if the something went wrong
377
+ */
378
+ getPluginVersion(): Promise<{
379
+ version: string;
380
+ }>;
372
381
  }
373
382
  /**
374
383
  * @interface IncomingCallPayload
@@ -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 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 | 'participant_counts'\n | 'missed'\n | 'accepted'\n | 'ended'\n | 'camera_enabled'\n | 'camera_disabled'\n | 'speaker_enabled'\n | 'speaker_disabled'\n | 'microphone_enabled'\n | 'microphone_disabled'\n | 'outgoing_call_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' | 'audio_room' | 'livestream' | 'development';\n\n/**\n * @typedef StreamCallLayout\n * @description Layout modes matching the Stream React SDK; includes native aliases.\n */\nexport type StreamCallLayout = 'grid' | 'spotlight' | 'dynamic' | 'fullScreen' | 'fullscreen';\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 custom?: Record<\n string,\n | string\n | boolean\n | number\n | null\n | Record<string, string | boolean | number | null>\n | string[]\n | boolean[]\n | number[]\n >;\n\n count?: number;\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface DynamicApiKeyResponse\n * @description Response from getDynamicStreamVideoApikey\n * @property {string|null} apiKey - The dynamic API key if set, null if not\n * @property {boolean} hasDynamicKey - Whether a dynamic key is currently set\n */\nexport interface DynamicApiKeyResponse {\n /** The dynamic API key if set, null if not */\n apiKey: string | null;\n /** Whether a dynamic key is currently set */\n hasDynamicKey: boolean;\n}\n\n/**\n * @interface CurrentUserResponse\n * @description Response from getCurrentUser containing user information\n * @property {string} userId - User ID of the current user\n * @property {string} name - Display name of the current user\n * @property {string} [imageURL] - Avatar URL of the current user\n * @property {boolean} isLoggedIn - Whether the user is currently logged in\n */\nexport interface CurrentUserResponse {\n /** User ID of the current user */\n userId: string;\n /** Display name of the current user */\n name: string;\n /** Avatar URL of the current user */\n imageURL?: string;\n /** Whether the user is currently logged in */\n isLoggedIn: 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 callId?: string;\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 /** Custom data to be passed to the call */\n custom?: Record<\n string,\n | string\n | boolean\n | number\n | null\n | Record<string, string | boolean | number | null>\n | string[]\n | boolean[]\n | number[]\n >;\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 * Join an existing call\n * @param {{ callId: string, callType: string }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.joinCall({ callId: 'call001', callType: 'default' });\n */\n joinCall?(options: { callId: string; callType: string }): 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 * Enable bluetooth audio\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.enableBluetooth();\n */\n enableBluetooth?(): Promise<SuccessResponse>;\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 * Get the current ringing call\n * @returns {Promise<CallEvent>} Current ringing call status as a CallEvent\n * @example\n * const ringingCall = await StreamCall.getRingingCall();\n * console.log(ringingCall);\n */\n getRingingCall?(): Promise<CallEvent>;\n\n /**\n * Cycle through the available video layouts\n * @returns {Promise<{ newLayout: StreamCallLayout }>} The layout that is now active\n * @example\n * const { newLayout } = await StreamCall.toggleViews();\n * console.log(`Layout switched to ${newLayout}`);\n */\n toggleViews?(): Promise<{ newLayout: StreamCallLayout }>;\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 * Set a dynamic Stream Video API key that overrides the static one\n * @param {{ apiKey: string }} options - The API key to set\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setDynamicStreamVideoApikey({ apiKey: 'new-api-key' });\n */\n setDynamicStreamVideoApikey(options: { apiKey: string }): Promise<SuccessResponse>;\n\n /**\n * Get the currently set dynamic Stream Video API key\n * @returns {Promise<DynamicApiKeyResponse>} The dynamic API key and whether it's set\n * @example\n * const result = await StreamCall.getDynamicStreamVideoApikey();\n * if (result.hasDynamicKey) {\n * console.log('Dynamic API key:', result.apiKey);\n * } else {\n * console.log('Using static API key from resources');\n * }\n */\n getDynamicStreamVideoApikey(): Promise<DynamicApiKeyResponse>;\n\n /**\n * Get the current user's information\n * @returns {Promise<CurrentUserResponse>} Current user information\n * @example\n * const currentUser = await StreamCall.getCurrentUser();\n * console.log(currentUser);\n */\n getCurrentUser(): Promise<CurrentUserResponse>;\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 /** Custom data to be passed to the call */\n custom?: Record<\n string,\n | string\n | boolean\n | number\n | null\n | Record<string, string | boolean | number | null>\n | string[]\n | boolean[]\n | number[]\n >;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\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 | 'participant_counts'\n | 'missed'\n | 'accepted'\n | 'ended'\n | 'camera_enabled'\n | 'camera_disabled'\n | 'speaker_enabled'\n | 'speaker_disabled'\n | 'microphone_enabled'\n | 'microphone_disabled'\n | 'outgoing_call_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' | 'audio_room' | 'livestream' | 'development';\n\n/**\n * @typedef StreamCallLayout\n * @description Layout modes matching the Stream React SDK; includes native aliases.\n */\nexport type StreamCallLayout = 'grid' | 'spotlight' | 'dynamic' | 'fullScreen' | 'fullscreen';\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 custom?: Record<\n string,\n | string\n | boolean\n | number\n | null\n | Record<string, string | boolean | number | null>\n | string[]\n | boolean[]\n | number[]\n >;\n\n count?: number;\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface DynamicApiKeyResponse\n * @description Response from getDynamicStreamVideoApikey\n * @property {string|null} apiKey - The dynamic API key if set, null if not\n * @property {boolean} hasDynamicKey - Whether a dynamic key is currently set\n */\nexport interface DynamicApiKeyResponse {\n /** The dynamic API key if set, null if not */\n apiKey: string | null;\n /** Whether a dynamic key is currently set */\n hasDynamicKey: boolean;\n}\n\n/**\n * @interface CurrentUserResponse\n * @description Response from getCurrentUser containing user information\n * @property {string} userId - User ID of the current user\n * @property {string} name - Display name of the current user\n * @property {string} [imageURL] - Avatar URL of the current user\n * @property {boolean} isLoggedIn - Whether the user is currently logged in\n */\nexport interface CurrentUserResponse {\n /** User ID of the current user */\n userId: string;\n /** Display name of the current user */\n name: string;\n /** Avatar URL of the current user */\n imageURL?: string;\n /** Whether the user is currently logged in */\n isLoggedIn: 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 callId?: string;\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 /** Custom data to be passed to the call */\n custom?: Record<\n string,\n | string\n | boolean\n | number\n | null\n | Record<string, string | boolean | number | null>\n | string[]\n | boolean[]\n | number[]\n >;\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 * Join an existing call\n * @param {{ callId: string, callType: string }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.joinCall({ callId: 'call001', callType: 'default' });\n */\n joinCall?(options: { callId: string; callType: string }): 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 * Enable bluetooth audio\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.enableBluetooth();\n */\n enableBluetooth?(): Promise<SuccessResponse>;\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 * Get the current ringing call\n * @returns {Promise<CallEvent>} Current ringing call status as a CallEvent\n * @example\n * const ringingCall = await StreamCall.getRingingCall();\n * console.log(ringingCall);\n */\n getRingingCall?(): Promise<CallEvent>;\n\n /**\n * Cycle through the available video layouts\n * @returns {Promise<{ newLayout: StreamCallLayout }>} The layout that is now active\n * @example\n * const { newLayout } = await StreamCall.toggleViews();\n * console.log(`Layout switched to ${newLayout}`);\n */\n toggleViews?(): Promise<{ newLayout: StreamCallLayout }>;\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 * Set a dynamic Stream Video API key that overrides the static one\n * @param {{ apiKey: string }} options - The API key to set\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setDynamicStreamVideoApikey({ apiKey: 'new-api-key' });\n */\n setDynamicStreamVideoApikey(options: { apiKey: string }): Promise<SuccessResponse>;\n\n /**\n * Get the currently set dynamic Stream Video API key\n * @returns {Promise<DynamicApiKeyResponse>} The dynamic API key and whether it's set\n * @example\n * const result = await StreamCall.getDynamicStreamVideoApikey();\n * if (result.hasDynamicKey) {\n * console.log('Dynamic API key:', result.apiKey);\n * } else {\n * console.log('Using static API key from resources');\n * }\n */\n getDynamicStreamVideoApikey(): Promise<DynamicApiKeyResponse>;\n\n /**\n * Get the current user's information\n * @returns {Promise<CurrentUserResponse>} Current user information\n * @example\n * const currentUser = await StreamCall.getCurrentUser();\n * console.log(currentUser);\n */\n getCurrentUser(): Promise<CurrentUserResponse>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\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 /** Custom data to be passed to the call */\n custom?: Record<\n string,\n | string\n | boolean\n | number\n | null\n | Record<string, string | boolean | number | null>\n | string[]\n | boolean[]\n | number[]\n >;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n"]}
@@ -12,7 +12,7 @@ import WebKit
12
12
  */
13
13
  @objc(StreamCallPlugin)
14
14
  public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
15
- private let PLUGIN_VERSION: String = "7.4.0"
15
+ private let pluginVersion: String = "7.7.7"
16
16
  public let identifier = "StreamCallPlugin"
17
17
  public let jsName = "StreamCall"
18
18
  public let pluginMethods: [CAPPluginMethod] = [
@@ -68,6 +68,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
68
68
 
69
69
  @Injected(\.callKitAdapter) var callKitAdapter
70
70
  @Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
71
+ @Injected(\.utils) var utils
72
+
71
73
  private var webviewDelegate: WebviewNavigationDelegate?
72
74
 
73
75
  // Declare as optional and initialize in load() method
@@ -227,13 +229,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
227
229
  "state": "rejected"
228
230
  ]
229
231
  notifyListeners("callEvent", data: data)
230
- case let .typeCallAcceptedEvent(response):
231
- if let streamUserId = self.streamVideo?.user.id,
232
- response.user.id == streamUserId,
233
- response.callCid != self.currentCallId {
234
-
235
- self.updateCallStatusAndNotify(callId: response.callCid, state: "joined")
236
- }
237
232
  case let .typeCallEndedEvent(response):
238
233
  let data: [String: Any] = [
239
234
  "callId": response.callCid,
@@ -283,6 +278,12 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
283
278
  viewModel.setActiveCall(activeCall)
284
279
  viewModel.update(participantsLayout: .grid)
285
280
 
281
+ if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
282
+ print("Notifying call joined: \(callId)")
283
+ self.updateCallStatusAndNotify(callId: callId, state: "joined")
284
+ self.hasNotifiedCallJoined = true
285
+ }
286
+
286
287
  // Subscribe to speaker status for this active call
287
288
  self.speakerSubscription = activeCall.speaker.$status
288
289
  .receive(on: DispatchQueue.main)
@@ -316,6 +317,17 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
316
317
  print("Active call became nil, cleaning up speaker subscription")
317
318
  self.speakerSubscription?.cancel()
318
319
  self.speakerSubscription = nil
320
+
321
+ print("Call actually ending: \(self.currentCallId)")
322
+
323
+ // Notify that call has ended - use the stored call ID
324
+ self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
325
+
326
+ // Reset notification flag when call ends
327
+ self.hasNotifiedCallJoined = false
328
+
329
+ // Remove the call overlay view and touch intercept view when not in a call
330
+ self.ensureViewRemoved()
319
331
  }
320
332
  }
321
333
 
@@ -340,6 +352,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
340
352
  case .outgoing = previousState {
341
353
  print("Call state changed from outgoing to idle. Ending call with ID: \(self.currentCallId)")
342
354
 
355
+ // Stop ongoing sound when call is cancelled
356
+ self.utils.callSoundsPlayer.stopOngoingSound()
357
+
343
358
  // End the call using the stored call ID and type
344
359
  if !self.currentCallId.isEmpty {
345
360
  Task {
@@ -389,6 +404,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
389
404
  // Create/update overlay and make visible when there's an active call
390
405
  self.createCallOverlayView()
391
406
 
407
+ // Stop ongoing sound when call is joined
408
+ self.utils.callSoundsPlayer.stopOngoingSound()
409
+
392
410
  // Notify that a call has started - but only if we haven't notified for this call yet
393
411
  if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
394
412
  print("Notifying call joined: \(callId)")
@@ -399,28 +417,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
399
417
  } else if case .incoming(let incomingCall) = newState {
400
418
  self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
401
419
  // }
402
- } else if newState == .idle {
403
- print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
404
-
405
- // Only notify about call ending if we have a valid stored call ID and there's truly no active call
406
- // This prevents false "left" events during normal state transitions
407
- if !self.currentCallId.isEmpty && self.streamVideo?.state.activeCall == nil {
408
- print("Call actually ending: \(self.currentCallId)")
409
-
410
- // Notify that call has ended - use the stored call ID
411
- self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
412
-
413
- // Reset notification flag when call ends
414
- self.hasNotifiedCallJoined = false
415
-
416
- // Remove the call overlay view and touch intercept view when not in a call
417
- self.ensureViewRemoved()
418
-
419
- } else {
420
- print("Not sending left event - CurrentCallId: \(self.currentCallId), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
421
- }
422
420
  }
423
-
424
421
  // Update the previous state for next comparison
425
422
  self.previousCallingState = newState
426
423
  } catch {
@@ -692,6 +689,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
692
689
  print("- Users: \(members)")
693
690
  print("- Should Ring: \(shouldRing)")
694
691
  print("- Team: \(String(describing: team))")
692
+ // Play outgoing call sound
693
+ self.utils.callSoundsPlayer.playOutgoingCallSound()
695
694
 
696
695
  // Create the call object
697
696
  await self.callViewModel?.startCall(
@@ -821,6 +820,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
821
820
  }
822
821
 
823
822
  private func endCallInternal() {
823
+ // Stop ongoing sound when call ends
824
+ self.utils.callSoundsPlayer.stopOngoingSound()
825
+
824
826
  do {
825
827
  try requireInitialized()
826
828
 
@@ -905,6 +907,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
905
907
  }
906
908
 
907
909
  @objc func endCall(_ call: CAPPluginCall) {
910
+ // Stop ongoing sound when call ends
911
+ self.utils.callSoundsPlayer.stopOngoingSound()
912
+
908
913
  do {
909
914
  try requireInitialized()
910
915
 
@@ -1631,7 +1636,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1631
1636
  }
1632
1637
 
1633
1638
  @objc func getPluginVersion(_ call: CAPPluginCall) {
1634
- call.resolve(["version": self.PLUGIN_VERSION])
1639
+ call.resolve(["version": self.pluginVersion])
1635
1640
  }
1636
1641
 
1637
1642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "7.4.0",
3
+ "version": "7.7.7",
4
4
  "description": "Uses the https://getstream.io/ SDK to implement calling in Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",