@capgo/capacitor-stream-call 0.0.43 → 0.0.51

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
@@ -140,123 +140,96 @@ Add string resources for different languages:
140
140
  </resources>
141
141
  ```
142
142
 
143
- ## Usage
143
+ ## Displaying Caller Information
144
144
 
145
- ### Handling Incoming Calls (Android)
146
- On Android, to handle incoming calls when the device is locked, you need to listen for the `incomingCall` event and manage the call state properly:
145
+ When receiving incoming calls, you can access caller information including name, user ID, and profile image. This information is automatically extracted from the call data and passed through the event system.
147
146
 
148
- ```typescript
149
- import { StreamCall } from '@capgo/capacitor-stream-call';
147
+ ### Getting Caller Information
150
148
 
151
- export class CallService {
152
- private incomingCallId: string | null = null;
153
- private isLockscreenIncoming = false;
149
+ The caller information is available in two ways:
154
150
 
155
- constructor() {
156
- this.setupIncomingCallListener();
157
- }
151
+ **1. Through Call Events**
158
152
 
159
- private setupIncomingCallListener() {
160
- StreamCall.addListener('incomingCall', async (payload) => {
161
- console.log('Incoming call from lockscreen:', payload);
162
-
163
- // Store the call ID and show incoming call UI
164
- this.incomingCallId = payload.cid;
165
- this.isLockscreenIncoming = true;
166
-
167
- // Show your custom incoming call screen
168
- this.showIncomingCallScreen(payload);
169
- });
170
- }
153
+ The `callEvent` listener provides caller information for incoming calls:
171
154
 
172
- // Accept the incoming call
173
- async acceptCall() {
174
- if (!this.incomingCallId) {
175
- console.warn('No incoming call to accept');
176
- return;
177
- }
178
-
179
- try {
180
- await StreamCall.acceptCall();
181
- this.clearIncomingCall();
182
- console.log('Call accepted successfully');
183
- } catch (error) {
184
- console.error('Failed to accept call:', error);
185
- }
186
- }
187
-
188
- // Reject the incoming call
189
- async rejectCall() {
190
- if (!this.incomingCallId) {
191
- console.warn('No incoming call to reject');
192
- return;
193
- }
194
-
195
- try {
196
- await StreamCall.rejectCall();
197
- this.clearIncomingCall();
198
- console.log('Call rejected successfully');
199
- } catch (error) {
200
- console.error('Failed to reject call:', error);
201
- }
155
+ ```typescript
156
+ StreamCall.addListener('callEvent', (event) => {
157
+ if (event.state === 'ringing' && event.caller) {
158
+ console.log('Incoming call from:', event.caller.name || event.caller.userId);
159
+ console.log('Caller image:', event.caller.imageURL);
160
+ // Update your UI to show caller information
161
+ showIncomingCallUI(event.caller);
202
162
  }
163
+ });
164
+ ```
203
165
 
204
- private clearIncomingCall() {
205
- this.incomingCallId = null;
206
- this.isLockscreenIncoming = false;
207
- // Hide your incoming call UI
208
- this.hideIncomingCallScreen();
209
- }
166
+ **2. Through Incoming Call Events (Android lock-screen)**
210
167
 
211
- private showIncomingCallScreen(payload: any) {
212
- // Implement your custom incoming call UI
213
- // This should show accept/reject buttons and caller information
214
- // Example: navigate to incoming call page or show modal
215
- }
168
+ The `incomingCall` listener also includes caller information:
216
169
 
217
- private hideIncomingCallScreen() {
218
- // Hide the incoming call UI
219
- // Example: navigate away from incoming call page or close modal
170
+ ```typescript
171
+ StreamCall.addListener('incomingCall', (payload) => {
172
+ if (payload.caller) {
173
+ console.log('Lock-screen call from:', payload.caller.name || payload.caller.userId);
174
+ // Update your lock-screen UI
175
+ updateLockScreenUI(payload.caller);
220
176
  }
221
- }
177
+ });
222
178
  ```
223
179
 
224
- > **Important:** This lock-screen handling is only required on Android. On iOS, the system handles incoming call UI automatically.
180
+ ### Caller Information Structure
225
181
 
226
- ### Basic Call Operations
227
182
  ```typescript
228
- import { StreamCall } from '@capgo/capacitor-stream-call';
229
-
230
- // Login to Stream Video
231
- await StreamCall.login({
232
- token: 'your_user_token',
233
- userId: 'user_id',
234
- name: 'User Name',
235
- apiKey: 'your_api_key',
236
- magicDivId: 'video-container'
237
- });
183
+ interface CallMember {
184
+ userId: string; // User ID (always present)
185
+ name?: string; // Display name (optional)
186
+ imageURL?: string; // Profile image URL (optional)
187
+ role?: string; // User role (optional)
188
+ }
189
+ ```
238
190
 
239
- // Make a call
240
- await StreamCall.call({
241
- userIds: ['user_to_call'],
242
- type: 'default',
243
- ring: true
244
- });
191
+ ### Example Implementation
245
192
 
246
- // End call
247
- await StreamCall.endCall();
193
+ Here's how to implement a proper incoming call screen with caller information:
248
194
 
249
- // Toggle microphone
250
- await StreamCall.setMicrophoneEnabled({ enabled: false });
195
+ ```typescript
196
+ export class CallService {
197
+ private callerInfo: CallMember | null = null;
251
198
 
252
- // Toggle camera
253
- await StreamCall.setCameraEnabled({ enabled: false });
199
+ constructor() {
200
+ this.setupCallListeners();
201
+ }
254
202
 
255
- // Switch camera
256
- await StreamCall.switchCamera({ camera: 'front' });
203
+ private setupCallListeners() {
204
+ StreamCall.addListener('callEvent', (event) => {
205
+ if (event.state === 'ringing') {
206
+ this.callerInfo = event.caller || null;
207
+ this.showIncomingCallScreen();
208
+ } else if (event.state === 'joined' || event.state === 'left') {
209
+ this.callerInfo = null;
210
+ this.hideIncomingCallScreen();
211
+ }
212
+ });
257
213
 
258
- // Logout
259
- await StreamCall.logout();
214
+ // Android lock-screen support
215
+ if (Capacitor.getPlatform() === 'android') {
216
+ StreamCall.addListener('incomingCall', (payload) => {
217
+ this.callerInfo = payload.caller || null;
218
+ this.showLockScreenIncomingCall();
219
+ });
220
+ }
221
+ }
222
+
223
+ private showIncomingCallScreen() {
224
+ const callerName = this.callerInfo?.name || 'Unknown Caller';
225
+ const callerImage = this.callerInfo?.imageURL || 'default-avatar.png';
226
+
227
+ // Update your UI components
228
+ document.getElementById('caller-name').textContent = callerName;
229
+ document.getElementById('caller-image').src = callerImage;
230
+ document.getElementById('incoming-call-screen').style.display = 'block';
231
+ }
232
+ }
260
233
  ```
261
234
 
262
235
  ## API
@@ -278,6 +251,7 @@ await StreamCall.logout();
278
251
  * [`getCallStatus()`](#getcallstatus)
279
252
  * [`setSpeaker(...)`](#setspeaker)
280
253
  * [`switchCamera(...)`](#switchcamera)
254
+ * [`getCallInfo(...)`](#getcallinfo)
281
255
  * [Interfaces](#interfaces)
282
256
  * [Type Aliases](#type-aliases)
283
257
  * [Enums](#enums)
@@ -515,6 +489,23 @@ Switch camera
515
489
  --------------------
516
490
 
517
491
 
492
+ ### getCallInfo(...)
493
+
494
+ ```typescript
495
+ getCallInfo(options: { callId: string; }) => Promise<CallEvent>
496
+ ```
497
+
498
+ Get detailed information about an active call including caller details
499
+
500
+ | Param | Type | Description |
501
+ | ------------- | -------------------------------- | -------------------------------- |
502
+ | **`options`** | <code>{ callId: string; }</code> | - Options containing the call ID |
503
+
504
+ **Returns:** <code>Promise&lt;<a href="#callevent">CallEvent</a>&gt;</code>
505
+
506
+ --------------------
507
+
508
+
518
509
  ### Interfaces
519
510
 
520
511
 
@@ -549,12 +540,14 @@ Switch camera
549
540
 
550
541
  #### CallEvent
551
542
 
552
- | Prop | Type | Description |
553
- | ------------ | ----------------------------------------------- | -------------------------------------------------------------- |
554
- | **`callId`** | <code>string</code> | ID of the call |
555
- | **`state`** | <code><a href="#callstate">CallState</a></code> | Current state of the call |
556
- | **`userId`** | <code>string</code> | User ID of the participant in the call who triggered the event |
557
- | **`reason`** | <code>string</code> | Reason for the call state change, if applicable |
543
+ | Prop | Type | Description |
544
+ | ------------- | ------------------------------------------------- | -------------------------------------------------------------- |
545
+ | **`callId`** | <code>string</code> | ID of the call |
546
+ | **`state`** | <code><a href="#callstate">CallState</a></code> | Current state of the call |
547
+ | **`userId`** | <code>string</code> | User ID of the participant in the call who triggered the event |
548
+ | **`reason`** | <code>string</code> | Reason for the call state change, if applicable |
549
+ | **`caller`** | <code><a href="#callmember">CallMember</a></code> | Information about the caller (for incoming calls) |
550
+ | **`members`** | <code>CallMember[]</code> | List of call members |
558
551
 
559
552
 
560
553
  #### CallState
@@ -748,12 +741,23 @@ The JSON representation for <a href="#listvalue">`ListValue`</a> is JSON array.
748
741
  | **`sessionId`** | <code>string</code> | the user sesion_id to pin, if not provided, applies to all sessions |
749
742
 
750
743
 
744
+ #### CallMember
745
+
746
+ | Prop | Type | Description |
747
+ | -------------- | ------------------- | ----------------------------- |
748
+ | **`userId`** | <code>string</code> | User ID of the member |
749
+ | **`name`** | <code>string</code> | Display name of the user |
750
+ | **`imageURL`** | <code>string</code> | Profile image URL of the user |
751
+ | **`role`** | <code>string</code> | Role of the user in the call |
752
+
753
+
751
754
  #### IncomingCallPayload
752
755
 
753
- | Prop | Type | Description |
754
- | ---------- | ----------------------- | ---------------------------------------- |
755
- | **`cid`** | <code>string</code> | Full call CID (e.g. default:123) |
756
- | **`type`** | <code>'incoming'</code> | Event type (currently always "incoming") |
756
+ | Prop | Type | Description |
757
+ | ------------ | ------------------------------------------------- | ---------------------------------------- |
758
+ | **`cid`** | <code>string</code> | Full call CID (e.g. default:123) |
759
+ | **`type`** | <code>'incoming'</code> | Event type (currently always "incoming") |
760
+ | **`caller`** | <code><a href="#callmember">CallMember</a></code> | Information about the caller |
757
761
 
758
762
 
759
763
  #### CameraEnabledResponse
@@ -63,6 +63,7 @@ import io.getstream.video.android.core.CameraDirection
63
63
  import android.content.BroadcastReceiver
64
64
  import android.content.Intent
65
65
  import android.content.IntentFilter
66
+ import com.getcapacitor.JSArray
66
67
  import io.getstream.android.video.generated.models.CallSessionParticipantLeftEvent
67
68
  import io.getstream.video.android.core.RealtimeConnection
68
69
  import io.getstream.video.android.core.events.ParticipantLeftEvent
@@ -215,18 +216,46 @@ public class StreamCallPlugin : Plugin() {
215
216
  // Start ringtone only; UI handled in web layer
216
217
  ringtonePlayer?.startRinging()
217
218
 
218
- // Notify WebView/JS about incoming call so it can render its own UI
219
- try {
220
- val payload = com.getcapacitor.JSObject().apply {
221
- put("cid", cid.cid)
222
- put("type", "incoming")
219
+ // Try to get caller information from the call
220
+ kotlinx.coroutines.GlobalScope.launch {
221
+ try {
222
+ val callInfo = call?.get()
223
+ val callerInfo = callInfo?.getOrNull()?.call?.createdBy
224
+
225
+ val payload = com.getcapacitor.JSObject().apply {
226
+ put("cid", cid.cid)
227
+ put("type", "incoming")
228
+ if (callerInfo != null) {
229
+ val caller = com.getcapacitor.JSObject().apply {
230
+ put("userId", callerInfo.id)
231
+ put("name", callerInfo.name ?: "")
232
+ put("imageURL", callerInfo.image ?: "")
233
+ put("role", callerInfo.role ?: "")
234
+ }
235
+ put("caller", caller)
236
+ }
237
+ }
238
+
239
+ // Notify WebView/JS about incoming call so it can render its own UI
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()
245
+ } catch (e: Exception) {
246
+ android.util.Log.e("StreamCallPlugin", "Error getting call info for incoming call", e)
247
+ // Fallback to basic payload without caller info
248
+ val payload = com.getcapacitor.JSObject().apply {
249
+ put("cid", cid.cid)
250
+ put("type", "incoming")
251
+ }
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()
223
257
  }
224
- notifyListeners("incomingCall", payload, true)
225
- } catch (e: Exception) {
226
- android.util.Log.e("StreamCallPlugin", "Error notifying JS about incoming call", e)
227
258
  }
228
-
229
- bringAppToForeground()
230
259
  } else {
231
260
  android.util.Log.w("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is null. Cannot process.")
232
261
  }
@@ -602,7 +631,38 @@ public class StreamCallPlugin : Plugin() {
602
631
  android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
603
632
  when (event) {
604
633
  is CallRingEvent -> {
605
- updateCallStatusAndNotify(event.callCid, "ringing")
634
+ // Extract caller information from the ringing call
635
+ kotlinx.coroutines.GlobalScope.launch {
636
+ try {
637
+ val callCid = event.callCid
638
+ val callIdParts = callCid.split(":")
639
+ if (callIdParts.size >= 2) {
640
+ val callType = callIdParts[0]
641
+ val callId = callIdParts[1]
642
+ val call = streamVideoClient?.call(type = callType, id = callId)
643
+ val callInfo = call?.get()
644
+ val callerInfo = callInfo?.getOrNull()?.call?.createdBy
645
+
646
+ // Pass caller information to the ringing event
647
+ if (callerInfo != null) {
648
+ val caller = mapOf(
649
+ "userId" to callerInfo.id,
650
+ "name" to (callerInfo.name ?: ""),
651
+ "imageURL" to (callerInfo.image ?: ""),
652
+ "role" to (callerInfo.role ?: "")
653
+ )
654
+ updateCallStatusAndNotify(event.callCid, "ringing", null, null, null, caller)
655
+ } else {
656
+ updateCallStatusAndNotify(event.callCid, "ringing")
657
+ }
658
+ } else {
659
+ updateCallStatusAndNotify(event.callCid, "ringing")
660
+ }
661
+ } catch (e: Exception) {
662
+ android.util.Log.e("StreamCallPlugin", "Error getting caller info for ringing event", e)
663
+ updateCallStatusAndNotify(event.callCid, "ringing")
664
+ }
665
+ }
606
666
  }
607
667
  // Handle CallCreatedEvent differently - only log it but don't try to access members yet
608
668
  is CallCreatedEvent -> {
@@ -611,22 +671,59 @@ public class StreamCallPlugin : Plugin() {
611
671
  android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: All members from event: ${event.members.joinToString { it.user.id + " (role: " + it.user.role + ")" }}")
612
672
  android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Self user ID from SDK: ${this@StreamCallPlugin.streamVideoClient?.userId}")
613
673
 
614
- val callParticipants = event.members.filter {
615
- val selfId = this@StreamCallPlugin.streamVideoClient?.userId
616
- val memberId = it.user.id
617
- val isSelf = memberId == selfId
618
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
619
- !isSelf
620
- }.map { it.user.id }
621
-
622
- android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
623
-
624
- // Start tracking this call now that we have the member list
625
- startCallTimeoutMonitor(callCid, callParticipants)
626
-
627
- // Use direction from event if available
628
- val callType = callCid.split(":").firstOrNull() ?: "default"
629
- updateCallStatusAndNotify(callCid, "created")
674
+ // Only send "created" event for outgoing calls (calls created by current user)
675
+ // For incoming calls, we'll only send "ringing" event in CallRingEvent handler
676
+ kotlinx.coroutines.GlobalScope.launch {
677
+ try {
678
+ val callIdParts = callCid.split(":")
679
+ if (callIdParts.size >= 2) {
680
+ val callType = callIdParts[0]
681
+ val callId = callIdParts[1]
682
+ val call = streamVideoClient?.call(type = callType, id = callId)
683
+ val callInfo = call?.get()
684
+ val createdBy = callInfo?.getOrNull()?.call?.createdBy
685
+ val currentUserId = streamVideoClient?.userId
686
+
687
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Call created by: ${createdBy?.id}, Current user: $currentUserId")
688
+
689
+ // Only notify for outgoing calls (where current user is the creator)
690
+ if (createdBy?.id == currentUserId) {
691
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: This is an outgoing call, sending created event")
692
+
693
+ val callParticipants = event.members.filter {
694
+ val selfId = this@StreamCallPlugin.streamVideoClient?.userId
695
+ val memberId = it.user.id
696
+ val isSelf = memberId == selfId
697
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
698
+ !isSelf
699
+ }.map { it.user.id }
700
+
701
+ android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
702
+
703
+ // Start tracking this call now that we have the member list
704
+ startCallTimeoutMonitor(callCid, callParticipants)
705
+
706
+ // Extract all members information (including self) for UI display
707
+ val allMembers = event.members.map { member ->
708
+ mapOf(
709
+ "userId" to member.user.id,
710
+ "name" to (member.user.name ?: ""),
711
+ "imageURL" to (member.user.image ?: ""),
712
+ "role" to (member.user.role ?: "")
713
+ )
714
+ }
715
+
716
+ updateCallStatusAndNotify(callCid, "created", null, null, allMembers)
717
+ } else {
718
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: This is an incoming call (created by ${createdBy?.id}), not sending created event")
719
+ }
720
+ } else {
721
+ android.util.Log.w("StreamCallPlugin", "CallCreatedEvent: Invalid call CID format: $callCid")
722
+ }
723
+ } catch (e: Exception) {
724
+ android.util.Log.e("StreamCallPlugin", "Error processing CallCreatedEvent", e)
725
+ }
726
+ }
630
727
  }
631
728
  // Add handler for CallSessionStartedEvent which contains participant information
632
729
  is CallSessionStartedEvent -> {
@@ -1153,7 +1250,35 @@ public class StreamCallPlugin : Plugin() {
1153
1250
  private suspend fun endCallRaw(call: Call) {
1154
1251
  val callId = call.id
1155
1252
  android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
1156
- call.leave()
1253
+
1254
+ try {
1255
+ // Get call information to make the decision
1256
+ val callInfo = call.get()
1257
+ val callData = callInfo?.getOrNull()?.call
1258
+ val currentUserId = streamVideoClient?.userId
1259
+ val createdBy = callData?.createdBy?.id
1260
+ val isCreator = createdBy == currentUserId
1261
+
1262
+ // Use call.state.totalParticipants to get participant count (as per StreamVideo Android SDK docs)
1263
+ val totalParticipants = call.state.totalParticipants.value ?: 0
1264
+ val shouldEndCall = isCreator || totalParticipants <= 2
1265
+
1266
+ android.util.Log.d("StreamCallPlugin", "Call $callId - Creator: $createdBy, CurrentUser: $currentUserId, IsCreator: $isCreator, TotalParticipants: $totalParticipants, ShouldEnd: $shouldEndCall")
1267
+
1268
+ if (shouldEndCall) {
1269
+ // End the call for everyone if I'm the creator or only 2 people
1270
+ android.util.Log.d("StreamCallPlugin", "Ending call $callId for all participants (creator: $isCreator, participants: $totalParticipants)")
1271
+ call.end()
1272
+ } else {
1273
+ // Just leave the call if there are more than 2 people and I'm not the creator
1274
+ android.util.Log.d("StreamCallPlugin", "Leaving call $callId (not creator, >2 participants)")
1275
+ call.leave()
1276
+ }
1277
+ } catch (e: Exception) {
1278
+ android.util.Log.e("StreamCallPlugin", "Error getting call info for $callId, defaulting to leave()", e)
1279
+ // Fallback to leave if we can't determine the call info
1280
+ call.leave()
1281
+ }
1157
1282
 
1158
1283
  // Capture context from the overlayView
1159
1284
  val currentContext = overlayView?.context ?: this.savedContext
@@ -1250,16 +1375,22 @@ public class StreamCallPlugin : Plugin() {
1250
1375
  @PluginMethod
1251
1376
  fun endCall(call: PluginCall) {
1252
1377
  try {
1253
- val activeCall = streamVideoClient?.state?.activeCall
1254
- if (activeCall == null) {
1255
- android.util.Log.w("StreamCallPlugin", "Attempted to end call but no active call found")
1378
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1379
+ val ringingCall = streamVideoClient?.state?.ringingCall?.value
1380
+
1381
+ val callToEnd = activeCall ?: ringingCall
1382
+
1383
+ if (callToEnd == null) {
1384
+ android.util.Log.w("StreamCallPlugin", "Attempted to end call but no active or ringing call found")
1256
1385
  call.reject("No active call to end")
1257
1386
  return
1258
1387
  }
1259
1388
 
1389
+ android.util.Log.d("StreamCallPlugin", "Ending call: activeCall=${activeCall?.id}, ringingCall=${ringingCall?.id}, callToEnd=${callToEnd.id}")
1390
+
1260
1391
  kotlinx.coroutines.GlobalScope.launch {
1261
1392
  try {
1262
- activeCall.value?.let { endCallRaw(it) }
1393
+ endCallRaw(callToEnd)
1263
1394
  call.resolve(JSObject().apply {
1264
1395
  put("success", true)
1265
1396
  })
@@ -1587,7 +1718,7 @@ public class StreamCallPlugin : Plugin() {
1587
1718
  }
1588
1719
 
1589
1720
  // Helper method to update call status and notify listeners
1590
- private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null) {
1721
+ private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null, members: List<Map<String, Any>>? = null, caller: Map<String, Any>? = null) {
1591
1722
  android.util.Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
1592
1723
  // Update stored call info
1593
1724
  currentCallId = callId
@@ -1608,6 +1739,26 @@ public class StreamCallPlugin : Plugin() {
1608
1739
  reason?.let {
1609
1740
  put("reason", it)
1610
1741
  }
1742
+ members?.let { membersList ->
1743
+ val membersArray = JSArray()
1744
+ membersList.forEach { member ->
1745
+ val memberObj = JSObject().apply {
1746
+ member.forEach { (key, value) ->
1747
+ put(key, value)
1748
+ }
1749
+ }
1750
+ membersArray.put(memberObj)
1751
+ }
1752
+ put("members", membersArray)
1753
+ }
1754
+ caller?.let { callerInfo ->
1755
+ val callerObj = JSObject().apply {
1756
+ callerInfo.forEach { (key, value) ->
1757
+ put(key, value)
1758
+ }
1759
+ }
1760
+ put("caller", callerObj)
1761
+ }
1611
1762
  }
1612
1763
 
1613
1764
  // Notify listeners