• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ringcentral / ringcentral-js-widgets / 5607299609

pending completion
5607299609

push

github

web-flow
sync features and bugfixs from dd3f20d7 (#1730)

8800 of 15728 branches covered (55.95%)

Branch coverage included in aggregate %.

881 of 1290 new or added lines in 248 files covered. (68.29%)

13 existing lines in 7 files now uncovered.

14708 of 22609 relevant lines covered (65.05%)

142084.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.69
/packages/ringcentral-integration/modules/ActiveCallControl/ActiveCallControl.ts
1
import { filter, find, forEach, isEmpty } from 'ramda';
2
import type { ActiveCallInfo, MakeCallParams } from 'ringcentral-call';
3
import { events as callEvents, RingCentralCall } from 'ringcentral-call';
4
import type { ReplyWithTextParams } from 'ringcentral-call-control/lib/Session';
5
import {
6
  PartyStatusCode,
7
  ReplyWithPattern,
8
} from 'ringcentral-call-control/lib/Session';
9
import { events as eventsEnum } from 'ringcentral-call/lib/Session';
10
import { v4 as uuidV4 } from 'uuid';
11
import type ExtensionTelephonySessionsEvent from '@rc-ex/core/lib/definitions/ExtensionTelephonySessionsEvent';
12
import {
13
  action,
14
  computed,
15
  RcModuleV2,
16
  state,
17
  storage,
18
  track,
19
  watch,
20
} from '@ringcentral-integration/core';
21
import { sleep } from '@ringcentral-integration/utils';
22

23
import { callDirection } from '../../enums/callDirections';
24
// eslint-disable-next-line import/no-named-as-default
25
import subscriptionFilters from '../../enums/subscriptionFilters';
26
import { trackEvents } from '../../enums/trackEvents';
27
import type {
28
  Session,
29
  WebphoneSession,
30
} from '../../interfaces/Webphone.interface';
31
import { Module } from '../../lib/di';
32
import { proxify } from '../../lib/proxy/proxify';
33
import { validateNumbers } from '../../lib/validateNumbers';
34
// TODO: should move that callErrors to enums
35
import { callErrors } from '../Call/callErrors';
36
import type { MessageBase } from '../Subscription';
37
import { sessionStatus } from '../Webphone/sessionStatus';
38
import { webphoneErrors } from '../Webphone/webphoneErrors';
39
import { normalizeSession as normalizeWebphoneSession } from '../Webphone/webphoneHelper';
40
import type {
41
  ActiveCallControlSessionData,
42
  ActiveSession,
43
  Deps,
44
  ICurrentDeviceCallsMap,
45
  ITransferCallSessionMapping,
46
  ModuleMakeCallParams,
47
  IPickUpCallDataMap,
48
  IPickUpCallParams,
49
} from './ActiveCallControl.interface';
50
import { callControlError } from './callControlError';
51
import {
52
  conflictError,
53
  filterDisconnectedCalls,
54
  isAtMainNumberPromptToneStage,
55
  isHolding,
56
  isOnRecording,
57
  isRecording,
58
  isRinging,
59
  normalizeSession,
60
  normalizeTelephonySession,
61
  getWebphoneReplyMessageOption,
62
} from './helpers';
63

64
const DEFAULT_TTL = 30 * 60 * 1000;
172✔
65
const DEFAULT_TIME_TO_RETRY = 62 * 1000;
172✔
66
const DEFAULT_BUSY_TIMEOUT = 3 * 1000;
172✔
67
const telephonySessionsEndPoint = /\/telephony\/sessions$/;
172✔
68
const subscribeEvent = subscriptionFilters.telephonySessions;
172✔
69

70
@Module({
71
  name: 'ActiveCallControl',
72
  deps: [
73
    'Auth',
74
    'Alert',
75
    'Brand',
76
    'Client',
77
    'Presence',
78
    'AccountInfo',
79
    'Subscription',
80
    'ExtensionInfo',
81
    'NumberValidate',
82
    'RegionSettings',
83
    'ConnectivityMonitor',
84
    'AppFeatures',
85
    { dep: 'Prefix', optional: true },
86
    { dep: 'Storage', optional: true },
87
    { dep: 'Webphone', optional: true },
88
    { dep: 'TabManager', optional: true },
89
    { dep: 'AudioSettings', optional: true },
90
    { dep: 'AvailabilityMonitor', optional: true },
91
    { dep: 'ActiveCallControlOptions', optional: true },
92
    { dep: 'RouterInteraction', optional: true },
93
  ],
94
})
95
export class ActiveCallControl extends RcModuleV2<Deps> {
96
  private _onCallEndFunc?: () => void;
97
  private _onCallSwitchedFunc?: (sessionId: string) => void;
98
  onCallIgnoreFunc?: (partyId: string) => void;
99
  private _connectivity = false;
266✔
100
  private _timeoutId: ReturnType<typeof setTimeout> | null = null;
266✔
101
  private _lastSubscriptionMessage: MessageBase | null = null;
266✔
102
  private _activeSession?: Session;
103

104
  private _ttl = this._deps.activeCallControlOptions?.ttl ?? DEFAULT_TTL;
266✔
105
  private _timeToRetry =
106
    this._deps.activeCallControlOptions?.timeToRetry ?? DEFAULT_TIME_TO_RETRY;
266✔
107
  private _polling = this._deps.activeCallControlOptions?.polling ?? false;
266✔
108
  private _promise: Promise<void> | null = null;
266✔
109
  private _rcCall: RingCentralCall | null = null;
266✔
110
  private _permissionCheck =
111
    this._deps.activeCallControlOptions?.permissionCheck ?? true;
266✔
112
  private _enableAutoSwitchFeature =
113
    this._deps.activeCallControlOptions?.enableAutoSwitchFeature ?? false;
266✔
114
  private _autoMergeSignCallIdKey = `${this._deps.prefix}-auto-merge-sign-call-id-key`;
266✔
115
  private _autoMergeCallsKey = `${this._deps.prefix}-auto-merge-calls-key`;
266✔
116
  private _autoMergeWebphoneSessionsMap = new Map<WebphoneSession, boolean>();
266✔
117

118
  @state
119
  pickUpCallDataMap: IPickUpCallDataMap = {};
266✔
120

121
  constructor(deps: Deps) {
122
    super({
266✔
123
      deps,
124
      enableCache: deps.activeCallControlOptions?.enableCache ?? true,
532✔
125
      storageKey: 'activeCallControl',
126
    });
127
  }
128

129
  override async onStateChange() {
130
    if (this.ready && this.hasPermission) {
135,542✔
131
      this._subscriptionHandler();
20,027✔
132
      this._checkConnectivity();
20,027✔
133
    }
134
  }
135

136
  override async _initModule() {
137
    this._createOtherInstanceListener();
263✔
138
    await super._initModule();
263✔
139
  }
140

141
  _createOtherInstanceListener() {
142
    if (!this._deps.tabManager || !this._enableAutoSwitchFeature) {
263!
143
      return;
263✔
144
    }
145
    window.addEventListener('storage', (e) => {
×
146
      this._onStorageChangeEvent(e);
×
147
    });
148
  }
149

150
  _onStorageChangeEvent(e: StorageEvent) {
151
    switch (e.key) {
×
152
      case this._autoMergeSignCallIdKey:
153
        this._triggerCurrentClientAutoMerge(e);
×
154
        break;
×
155
      case this._autoMergeCallsKey:
156
        this._autoMergeCallsHandler(e);
×
157
        break;
×
158
      default:
159
        break;
×
160
    }
161
  }
162

163
  _triggerCurrentClientAutoMerge(e: StorageEvent) {
164
    try {
×
165
      const { telephoneSessionId }: { telephoneSessionId: string } = JSON.parse(
×
166
        e.newValue!,
167
      );
168
      const ids = this.rcCallSessions
×
169
        .filter(
170
          (s) =>
171
            !isRinging(s) &&
×
172
            !!s.webphoneSession &&
173
            s.telephonySessionId !== telephoneSessionId,
174
        )
175
        .map((s) => s.telephonySessionId);
×
176
      const id = uuidV4();
×
177
      const data = { id, ids };
×
178
      if (ids.length) {
×
179
        localStorage.setItem(this._autoMergeCallsKey, JSON.stringify(data));
×
180
      }
181
    } catch (err) {
182
      console.log('AutoMerge sign event parse error');
×
183
    }
184
  }
185

186
  async _autoMergeCallsHandler(e: StorageEvent) {
187
    if (!this._deps.tabManager?.active) return;
×
188

189
    try {
×
190
      const { ids }: { ids: string[] } = JSON.parse(e.newValue!);
×
191
      const response = await this._deps.client.service
×
192
        .platform()
193
        .get(subscriptionFilters.detailedPresence);
194
      const data = await response.json();
×
195
      const activeCalls: ActiveCallInfo[] = data.activeCalls;
×
196
      const callsList = ids
×
197
        // filter calls which are already in current instance.
198
        .filter((id) =>
199
          this.sessions.find(
×
200
            (item: ActiveCallControlSessionData) =>
201
              item.telephonySessionId === id &&
×
202
              !!item.telephonySession &&
203
              !isEmpty(item.telephonySession),
204
          ),
205
        )
206
        // transfer id to ActiveCallInfo.
207
        .reduce((acc, telephonySessionId: string) => {
208
          const activeCall = activeCalls.find(
×
209
            (call) => call.telephonySessionId === telephonySessionId,
×
210
          );
211

212
          if (!activeCall) {
×
213
            console.log(
×
214
              `Auto Switch failed with telephonySessionId ${telephonySessionId}`,
215
            );
216
            return acc;
×
217
          }
218

219
          acc.push(activeCall);
×
220
          return acc;
×
221
        }, [] as ActiveCallInfo[]);
222

223
      if (callsList.length) {
×
224
        await Promise.all(
×
225
          callsList.map(async (activeCall) => {
226
            await this.transferUnmuteHandler(activeCall.telephonySessionId);
×
227
            const switchSession = this._rcCall!.switchCallFromActiveCall(
×
228
              activeCall,
229
              {
230
                homeCountryId: this._deps.regionSettings.homeCountryId,
231
              },
232
            );
233
            this._autoMergeWebphoneSessionsMap.set(
×
234
              switchSession.webphoneSession as WebphoneSession,
235
              true,
236
            );
237
            switchSession.webphoneSession.mute();
×
238
            switchSession.webphoneSession.once('accepted', async () => {
×
239
              switchSession.webphoneSession.unmute();
×
240
              await switchSession.webphoneSession.hold();
×
241
              this._addTrackToActiveSession();
×
242
            });
243
          }),
244
        );
245
      }
246
    } catch (err) {
247
      console.log(err);
×
248
      console.log('auto merge calls from other tabs failed');
×
249
    }
250
  }
251

252
  _triggerAutoMergeEvent(telephoneSessionId?: string) {
253
    if (!this._deps.tabManager || !this._enableAutoSwitchFeature) return;
×
254

255
    const id = uuidV4();
×
256
    const data = {
×
257
      id,
258
      telephoneSessionId,
259
    };
260
    localStorage.setItem(this._autoMergeSignCallIdKey, JSON.stringify(data));
×
261
  }
262

263
  _addTrackToActiveSession() {
264
    const telephonySessionId = this.activeSessionId;
×
265
    const activeRCCallSession =
266
      this.rcCallSessions.find(
×
267
        (s) => s.telephonySessionId === telephonySessionId,
×
268
      ) || this._activeSession;
269
    if (
×
270
      activeRCCallSession &&
×
271
      activeRCCallSession.webphoneSession &&
272
      this._deps.webphone
273
    ) {
274
      const { _remoteVideo, _localVideo } = this._deps.webphone;
×
275
      activeRCCallSession.webphoneSession.addTrack(_remoteVideo, _localVideo);
×
276
    }
277
  }
278

279
  @storage
280
  @state
281
  transferCallMapping: ITransferCallSessionMapping = {};
266✔
282

283
  @storage
284
  @state
285
  data: {
286
    activeSessionId: string | null;
287
    busyTimestamp: number;
288
    timestamp: number;
289
    sessions: ActiveCallControlSessionData[];
290
    ringSessionId: string | null;
291
  } = {
266✔
292
    activeSessionId: null,
293
    busyTimestamp: 0,
294
    timestamp: 0,
295
    sessions: [],
296
    ringSessionId: null,
297
  };
298

299
  @state
300
  currentDeviceCallsMap: ICurrentDeviceCallsMap = {};
266✔
301

302
  @state
303
  lastEndedSessionIds: string[] = [];
266✔
304

305
  // TODO: conference call using
306
  @state
307
  cachedSessions: object[] = [];
266✔
308

309
  override async onInit() {
310
    if (!this.hasPermission) return;
265✔
311

312
    await this._deps.subscription.subscribe([subscribeEvent]);
264✔
313
    this._rcCall = this._initRcCall();
264✔
314

315
    if (this._shouldFetch()) {
264!
316
      try {
264✔
317
        await this.fetchData();
264✔
318
      } catch (e: any /** TODO: confirm with instanceof */) {
319
        this._retry();
×
320
      }
321
    } else if (this._polling) {
×
322
      this._startPolling();
×
323
    } else {
324
      this._retry();
×
325
    }
326
  }
327

328
  private _initRcCall() {
329
    const rcCall = new RingCentralCall({
264✔
330
      sdk: this._deps.client.service,
331
      subscriptions: undefined,
332
      enableSubscriptionHander: false,
333
      callControlOptions: {
334
        preloadDevices: false,
335
        preloadSessions: false,
336
        extensionInfo: {
337
          ...this._deps.extensionInfo.info,
338
          // TODO: add info type in 'AccountInfo'
339
          // @ts-ignore
340
          account: this._deps.accountInfo.info,
341
        },
342
      },
343
      webphone: this._deps.webphone?._webphone!,
344
    });
345
    rcCall.on(callEvents.NEW, (session: Session) => {
264✔
346
      this._newSessionHandler(session);
26✔
347
    });
348
    rcCall.on(callEvents.WEBPHONE_INVITE, (session: WebphoneSession) =>
264✔
349
      this._onWebphoneInvite(session),
13✔
350
    );
351
    rcCall.on(callEvents.WEBPHONE_INVITE_SENT, (session: WebphoneSession) =>
264✔
352
      this._onWebphoneInvite(session),
×
353
    );
354
    // TODO: workaround of bug:
355
    // WebRTC outbound call with wrong sequences of telephony sessions then call log section will not show
356
    // @ts-ignore
357
    rcCall._callControl?.on('new', (session: Session) =>
264✔
358
      this._onNewCall(session),
13✔
359
    );
360

361
    return rcCall;
264✔
362
  }
363

364
  override onInitOnce() {
365
    if (this._deps.availabilityMonitor && this._deps.tabManager) {
263!
366
      watch(
263✔
367
        this,
368
        () => this.currentDeviceCallsMap,
75,488✔
369
        () => {
370
          const hasCallSession = Object.values(this.currentDeviceCallsMap).some(
570✔
371
            (webphoneSession) => !!webphoneSession,
58✔
372
          );
373
          const key = `acc-${this._deps.tabManager!.id}`;
570✔
374
          this._deps.availabilityMonitor!.setSharedState(key, {
570✔
375
            hasCallSession,
376
          });
377
        },
378
      );
379
    }
380
    if (this._deps.webphone) {
263!
381
      watch(
263✔
382
        this,
383
        () => this._deps.webphone?.connected,
75,488✔
384
        (newValue) => {
385
          if (newValue && this._deps.webphone?._webphone) {
416✔
386
            this._rcCall?.setWebphone(this._deps.webphone._webphone);
184✔
387
          }
388
        },
389
      );
390

391
      watch(
263✔
392
        this,
393
        () => this.activeSessionId,
75,488✔
394
        () => {
395
          this._addTrackToActiveSession();
×
396
        },
397
      );
398
    }
399
  }
400

401
  override onReset() {
402
    this.resetState();
232✔
403
  }
404

405
  @action
406
  resetState() {
407
    this.data.activeSessionId = null;
232✔
408
    this.data.busyTimestamp = 0;
232✔
409
    this.data.timestamp = 0;
232✔
410
    this.data.sessions = [];
232✔
411
  }
412

413
  _shouldFetch() {
414
    return !this._deps.tabManager || this._deps.tabManager.active;
264✔
415
  }
416

417
  @proxify
418
  async fetchData() {
419
    if (!this._promise) {
531!
420
      this._promise = this._fetchData();
531✔
421
    }
422
    await this._promise;
531✔
423
  }
424

425
  _clearTimeout() {
426
    if (this._timeoutId) clearTimeout(this._timeoutId);
×
427
  }
428

429
  _subscriptionHandler() {
430
    let { message } = this._deps.subscription;
20,027✔
431
    if (
20,027✔
432
      message &&
25,031✔
433
      // FIXME: is that object compare is fine, should confirm that?
434
      message !== this._lastSubscriptionMessage &&
435
      message.event &&
436
      telephonySessionsEndPoint.test(message.event) &&
437
      message.body
438
    ) {
439
      message = this._checkRingOutCallDirection(message);
13✔
440
      this._lastSubscriptionMessage = message;
13✔
441
      if (this._rcCall) {
13!
442
        this._rcCall.onNotificationEvent(message);
13✔
443
      }
444
    }
445
  }
446

447
  // TODO: workaround of PLA bug: https://jira_domain/browse/PLA-52742, remove these code after PLA
448
  // fixed this bug
449
  private _checkRingOutCallDirection(message: ExtensionTelephonySessionsEvent) {
450
    const { body } = message;
13✔
451
    const originType = body?.origin?.type;
13✔
452

453
    if (body && originType === 'RingOut') {
13!
454
      const { parties } = body;
×
455
      if (Array.isArray(parties) && parties.length) {
×
456
        forEach((party: any) => {
×
457
          if (
×
458
            party.ringOutRole &&
×
459
            party.ringOutRole === 'Initiator' &&
460
            party.direction === 'Inbound'
461
          ) {
462
            const tempFrom = { ...party.from };
×
463
            party.direction = 'Outbound';
×
464
            party.from = party.to;
×
465
            party.to = tempFrom;
×
466
          }
467
        }, parties);
468
      }
469
    }
470
    return message;
13✔
471
  }
472

473
  private _retry(t = this.timeToRetry) {
×
474
    this._clearTimeout();
×
475
    this._timeoutId = setTimeout(() => {
×
476
      this._timeoutId = null;
×
477
      if (!this.timestamp || Date.now() - this.timestamp > this.ttl) {
×
478
        if (!this._deps.tabManager || this._deps.tabManager.active) {
×
479
          this.fetchData();
×
480
        } else {
481
          // continue retry checks in case tab becomes main tab
482
          this._retry();
×
483
        }
484
      }
485
    }, t);
486
  }
487

488
  @proxify
489
  async _fetchData() {
490
    try {
531✔
491
      await this._syncData();
531✔
492
      if (this._polling) {
531!
493
        this._startPolling();
×
494
      }
495
      this._promise = null;
531✔
496
    } catch (error: any /** TODO: confirm with instanceof */) {
497
      this._promise = null;
×
498
      if (this._polling) {
×
499
        this._startPolling(this.timeToRetry);
×
500
      } else {
501
        this._retry();
×
502
      }
503
      throw error;
×
504
    }
505
  }
506

507
  private _startPolling(t = this.timestamp + this.ttl + 10 - Date.now()) {
×
508
    this._clearTimeout();
×
509
    this._timeoutId = setTimeout(() => {
×
510
      this._timeoutId = null;
×
511
      if (!this._deps.tabManager || this._deps.tabManager?.active) {
×
512
        if (!this.timestamp || Date.now() - this.timestamp > this.ttl) {
×
513
          this.fetchData();
×
514
        } else {
515
          this._startPolling();
×
516
        }
517
      } else if (this.timestamp && Date.now() - this.timestamp < this.ttl) {
×
518
        this._startPolling();
×
519
      } else {
520
        this._startPolling(this.timeToRetry);
×
521
      }
522
    }, t);
523
  }
524

525
  async _syncData() {
526
    try {
531✔
527
      const activeCalls = this._deps.presence.calls;
531✔
528
      await this._rcCall!.loadSessions(activeCalls);
531✔
529
      this.updateActiveSessions();
531✔
530
      this._rcCall!.sessions.forEach((session) => {
531✔
531
        this._newSessionHandler(session as Session);
×
532
      });
533
    } catch (error: any /** TODO: confirm with instanceof */) {
534
      console.log('sync data error:', error);
×
535
      throw error;
×
536
    }
537
  }
538

539
  private _onSessionDisconnected = () => {
266✔
540
    this.updateActiveSessions();
×
541
    if (!this._deps.tabManager || this._deps.tabManager?.active) {
×
542
      this.cleanCurrentWarmTransferData();
×
543
    }
544
  };
545

546
  private _updateSessionsStatusHandler = ({
266✔
547
    status,
548
    telephonySessionId,
549
  }: {
550
    status: PartyStatusCode;
551
    telephonySessionId: string;
552
  }) => {
553
    this.updateActiveSessions();
×
554

555
    if (
×
556
      status === PartyStatusCode.answered &&
×
557
      this.activeSessionId !== telephonySessionId
558
    ) {
559
      this.setActiveSessionId(telephonySessionId);
×
560
    }
561
  };
562

563
  private _updateSessionsHandler = () => {
266✔
564
    this.updateActiveSessions();
26✔
565
  };
566

567
  @proxify
568
  async _onNewCall(session: Session) {
569
    this.updateActiveSessions();
13✔
570
    const ringSession = find(
13✔
571
      (x) => isRinging(x) && x.id === session.id,
19✔
572
      this.sessions,
573
    );
574
    const sessionId = ringSession?.id;
13✔
575

576
    this._setRingSessionId(sessionId);
13✔
577
  }
578

579
  @action
580
  private _onCallAccepted(telephonySessionId: string) {
581
    if (this.ringSessionId === telephonySessionId) {
×
582
      this.data.ringSessionId = this.ringSessions[0]?.id || null;
×
583
    }
584
  }
585

586
  @action
587
  private _onCallEnd(telephonySessionId: string) {
588
    if (this.ringSessionId === telephonySessionId) {
×
589
      this.data.ringSessionId = this.ringSessions[0]?.id || null;
×
590
    }
591
  }
592

593
  updateActiveSessions() {
594
    const currentDeviceCallsMap: ICurrentDeviceCallsMap = {};
571✔
595
    const callControlSessions = (this._rcCall?.sessions || [])
571!
596
      .filter((session) => filterDisconnectedCalls(session))
71✔
597
      .map((session) => {
598
        // @ts-expect-error
599
        currentDeviceCallsMap[session.telephonySessionId] =
71✔
600
          // @ts-expect-error
601
          normalizeWebphoneSession(session.webphoneSession);
602

603
        return {
71✔
604
          ...session.data,
605
          activeCallId: session.activeCallId,
606
          direction: session.direction,
607
          from: session.from,
608
          id: session.id,
609
          otherParties: session.otherParties,
610
          party: session.party || {},
93✔
611
          recordings: session.recordings,
612
          isRecording: isOnRecording(session.recordings),
613
          sessionId: session.sessionId,
614
          startTime: session.startTime,
615
          status: session.status,
616
          telephonySessionId: session.telephonySessionId,
617
          telephonySession: normalizeTelephonySession(session.telephonySession),
618
          to: session.to,
619
        };
620
      });
621
    this._updateActiveSessions(currentDeviceCallsMap, callControlSessions);
571✔
622
  }
623

624
  @action
625
  private _updateActiveSessions(
626
    currentDeviceCallsMap: ICurrentDeviceCallsMap,
627
    callControlSessions: ActiveCallControlSessionData[],
628
  ) {
629
    this.data.timestamp = Date.now();
571✔
630
    this.currentDeviceCallsMap = currentDeviceCallsMap;
571✔
631
    this.data.sessions = callControlSessions || [];
571!
632
  }
633

634
  private _newSessionHandler(session: Session) {
635
    session.removeListener(
26✔
636
      eventsEnum.STATUS,
637
      this._updateSessionsStatusHandler,
638
    );
639
    session.removeListener(eventsEnum.MUTED, this._updateSessionsHandler);
26✔
640
    session.removeListener(eventsEnum.RECORDINGS, this._updateSessionsHandler);
26✔
641
    session.removeListener(
26✔
642
      eventsEnum.DISCONNECTED,
643
      this._onSessionDisconnected,
644
    );
645
    session.removeListener(
26✔
646
      eventsEnum.WEBPHONE_SESSION_CONNECTED,
647
      this._updateSessionsHandler,
648
    );
649
    session.on(eventsEnum.STATUS, this._updateSessionsStatusHandler);
26✔
650
    session.on(eventsEnum.MUTED, this._updateSessionsHandler);
26✔
651
    session.on(eventsEnum.RECORDINGS, this._updateSessionsHandler);
26✔
652
    session.on(eventsEnum.DISCONNECTED, this._onSessionDisconnected);
26✔
653
    session.on(
26✔
654
      eventsEnum.WEBPHONE_SESSION_CONNECTED,
655
      this._updateSessionsHandler,
656
    );
657
    // Handle the session update at the end of function to reduce the probability of empty rc call
658
    // sessions
659
    this._updateSessionsHandler();
26✔
660
  }
661

662
  @action
663
  removeActiveSession() {
664
    this.data.activeSessionId = null;
1✔
665
  }
666

667
  // count it as load (should only call on container init step)
668
  @action
669
  setActiveSessionId(telephonySessionId: string) {
670
    if (!telephonySessionId) return;
1!
671
    this.data.activeSessionId = telephonySessionId;
1✔
672
  }
673

674
  @action
675
  setLastEndedSessionIds(session: WebphoneSession) {
676
    /**
677
     * don't add incoming call that isn't relied by current app
678
     *   to end sessions. this call can be answered by other apps
679
     */
680
    const normalizedWebphoneSession = normalizeWebphoneSession(session);
×
681
    if (
×
682
      // @ts-expect-error
683
      !normalizedWebphoneSession.startTime &&
×
684
      // @ts-expect-error
685
      !normalizedWebphoneSession.isToVoicemail &&
686
      // @ts-expect-error
687
      !normalizedWebphoneSession.isForwarded &&
688
      // @ts-expect-error
689
      !normalizedWebphoneSession.isReplied
690
    ) {
691
      return;
×
692
    }
693
    // @ts-expect-error
694
    const { partyData } = normalizedWebphoneSession;
×
695
    if (!partyData) return;
×
696
    if (this.lastEndedSessionIds.indexOf(partyData.sessionId) === -1) {
×
697
      this.lastEndedSessionIds = [partyData.sessionId]
×
698
        .concat(this.lastEndedSessionIds)
699
        .slice(0, 5);
700
    }
701
  }
702

703
  private _checkConnectivity() {
704
    if (
20,027✔
705
      this._deps.connectivityMonitor &&
60,081✔
706
      this._deps.connectivityMonitor.ready &&
707
      this._connectivity !== this._deps.connectivityMonitor.connectivity
708
    ) {
709
      this._connectivity = this._deps.connectivityMonitor.connectivity;
271✔
710

711
      if (this._connectivity) {
271✔
712
        this.fetchData();
267✔
713
      }
714
    }
715
  }
716

717
  private _getTrackEventName(name: string) {
718
    // TODO: refactor to remove `this.parentModule`.
719
    const currentPath = this._deps.routerInteraction?.currentPath;
×
720
    const showCallLog = (this.parentModule as any).callLogSection?.show;
×
721
    const showNotification = (this.parentModule as any).callLogSection
×
722
      ?.showNotification;
723
    if (showNotification) {
×
724
      return `${name}/Call notification page`;
×
725
    }
726
    if (showCallLog) {
×
727
      return `${name}/Call log page`;
×
728
    }
729
    if (currentPath === '/calls') {
×
730
      return `${name}/All calls page`;
×
731
    }
732
    if (currentPath.includes('/simplifycallctrl')) {
×
733
      return `${name}/Small call control`;
×
734
    }
735
    return name;
×
736
  }
737

738
  @action
739
  setCallControlBusyTimestamp() {
740
    this.data.busyTimestamp = Date.now();
1✔
741
  }
742

743
  @action
744
  clearCallControlBusyTimestamp() {
745
    this.data.busyTimestamp = 0;
1✔
746
  }
747

748
  @track((that: ActiveCallControl) => [
×
749
    that._getTrackEventName(trackEvents.mute),
750
  ])
751
  @proxify
752
  async mute(telephonySessionId: string) {
753
    try {
×
754
      this.setCallControlBusyTimestamp();
×
755
      const session = this._getSessionById(telephonySessionId);
×
756
      await session.mute();
×
757
      this.clearCallControlBusyTimestamp();
×
758
    } catch (error: any) {
759
      // TODO: fix error handling with instanceof
760
      if (error.response && !error.response._text) {
×
761
        error.response._text = await error.response.clone().text();
×
762
      }
763
      if (conflictError(error)) {
×
764
        this._deps.alert.warning({
×
765
          message: callControlError.muteConflictError,
766
        });
767
      } else if (
×
768
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
769
      ) {
770
        this._deps.alert.warning({ message: callControlError.generalError });
×
771
      }
772
      this.clearCallControlBusyTimestamp();
×
773
    }
774
  }
775

776
  @track((that: ActiveCallControl) => [
×
777
    that._getTrackEventName(trackEvents.unmute),
778
  ])
779
  @proxify
780
  async unmute(telephonySessionId: string) {
781
    try {
×
782
      this.setCallControlBusyTimestamp();
×
783
      const session = this._getSessionById(telephonySessionId);
×
784
      await session.unmute();
×
785
      this.clearCallControlBusyTimestamp();
×
786
    } catch (error: any) {
787
      // TODO: fix error handling with instanceof
788
      if (error.response && !error.response._text) {
×
789
        error.response._text = await error.response.clone().text();
×
790
      }
791
      if (conflictError(error)) {
×
792
        this._deps.alert.warning({
×
793
          message: callControlError.unMuteConflictError,
794
        });
795
      } else if (
×
796
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
797
      ) {
798
        this._deps.alert.warning({ message: callControlError.generalError });
×
799
      }
800
      this.clearCallControlBusyTimestamp();
×
801
    }
802
  }
803

804
  async transferUnmuteHandler(telephonySessionId: string) {
805
    try {
×
806
      const session = this._getSessionById(telephonySessionId);
×
807
      if (session?.telephonySession?.party?.muted) {
×
808
        await session.unmute();
×
809
      }
810
    } catch (error: any /** TODO: confirm with instanceof */) {
811
      // https://jira_domain/browse/NTP-1308
812
      // Unmute before transfer due to we can not sync the mute status after transfer.
813
    }
814
  }
815

816
  @track((that: ActiveCallControl) => [
×
817
    that._getTrackEventName(trackEvents.record),
818
  ])
819
  @proxify
820
  async startRecord(telephonySessionId: string) {
821
    try {
×
822
      this.setCallControlBusyTimestamp();
×
823
      const session = this._getSessionById(telephonySessionId);
×
824
      const recordingId = this.getRecordingId(session);
×
825
      await session.startRecord({ recordingId });
×
826
      this.clearCallControlBusyTimestamp();
×
827
      return true;
×
828
    } catch (error: any) {
829
      // TODO: fix error handling with instanceof
830
      this.clearCallControlBusyTimestamp();
×
831
      const { errors = [] } = (await error.response.clone().json()) || {};
×
832
      if (errors.length) {
×
833
        for (const error of errors) {
×
834
          console.error('record fail:', error);
×
835
        }
836
        this._deps.alert.danger({
×
837
          message: webphoneErrors.recordError,
838
          payload: {
839
            errorCode: errors[0].errorCode,
840
          },
841
        });
842
      }
843
    }
844
  }
845

846
  getRecordingId(session: Session) {
847
    const recording = session.recordings[0];
×
848
    const recodingId = recording && recording.id;
×
849
    return recodingId;
×
850
  }
851

852
  @track((that: ActiveCallControl) => [
×
853
    that._getTrackEventName(trackEvents.stopRecord),
854
  ])
855
  @proxify
856
  async stopRecord(telephonySessionId: string) {
857
    try {
×
858
      this.setCallControlBusyTimestamp();
×
859
      const session = this._getSessionById(telephonySessionId);
×
860
      const recordingId = this.getRecordingId(session);
×
861
      await session.stopRecord({ recordingId });
×
862
      this.clearCallControlBusyTimestamp();
×
863
    } catch (error: any /** TODO: confirm with instanceof */) {
864
      console.log('stop record error:', error);
×
865

866
      this._deps.alert.danger({
×
867
        message: webphoneErrors.pauseRecordError,
868
      });
869

870
      this.clearCallControlBusyTimestamp();
×
871
    }
872
  }
873

874
  @track((that: ActiveCallControl) => [
×
875
    that._getTrackEventName(trackEvents.hangup),
876
  ])
877
  @proxify
878
  async hangUp(telephonySessionId: string) {
879
    try {
×
880
      this.setCallControlBusyTimestamp();
×
881
      const session = this._getSessionById(telephonySessionId);
×
882
      await session.hangup();
×
883

884
      this._onCallEndFunc?.();
×
885
      // TODO: find way to fix that 800ms
886
      // avoid hung up sync slow and user click multiple times.
887
      await sleep(800);
×
888
      this.clearCallControlBusyTimestamp();
×
889
    } catch (error: any) {
890
      // TODO: fix error handling with instanceof
891
      console.error('hangup error', error);
×
892
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
893
        this._deps.alert.warning({ message: callControlError.generalError });
×
894
      }
895
      this.clearCallControlBusyTimestamp();
×
896
    }
897
  }
898

899
  @track((that: ActiveCallControl) => [
×
900
    that._getTrackEventName(trackEvents.voicemail),
901
  ])
902
  @proxify
903
  async reject(telephonySessionId: string) {
904
    try {
×
905
      this.setCallControlBusyTimestamp();
×
906
      const session = this._getSessionById(telephonySessionId);
×
907

908
      // !If is a queue call, ignore is performed
909
      if (session.party.queueCall) {
×
910
        return await this.ignore(telephonySessionId);
×
911
      }
912

913
      await session.toVoicemail();
×
914

915
      if (session && session.webphoneSession) {
×
916
        (session.webphoneSession as WebphoneSession).__rc_isToVoicemail = true;
×
917
      }
918
      this.clearCallControlBusyTimestamp();
×
919
    } catch (error: any) {
920
      // TODO: fix error handling with instanceof
921
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
922
        this._deps.alert.warning({ message: callControlError.generalError });
×
923
      }
924
      this.clearCallControlBusyTimestamp();
×
925
    }
926
  }
927

928
  @track((that: ActiveCallControl) => [
×
929
    that._getTrackEventName(trackEvents.confirmSwitch),
930
  ])
931
  @proxify
932
  async switch(telephonySessionId: string) {
933
    try {
×
934
      this.setCallControlBusyTimestamp();
×
935
      await this.transferUnmuteHandler(telephonySessionId);
×
936
      const switchedSession = await this._rcCall!.switchCall(
×
937
        telephonySessionId,
938
        {
939
          homeCountryId: this._deps.regionSettings.homeCountryId,
940
        },
941
      );
942
      this._triggerAutoMergeEvent(telephonySessionId);
×
943
      await this._holdOtherCalls(telephonySessionId);
×
944
      this.clearCallControlBusyTimestamp();
×
945
      this._onCallSwitchedFunc?.(switchedSession.sessionId);
×
946
    } catch (error: any) {
947
      // TODO: fix error handling with instanceof
948
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
949
        this._deps.alert.warning({ message: callControlError.generalError });
×
950
      }
951
      this.clearCallControlBusyTimestamp();
×
952
    }
953
  }
954

955
  @track((that: ActiveCallControl) => [
×
956
    that._getTrackEventName(trackEvents.hold),
957
  ])
958
  @proxify
959
  async hold(telephonySessionId: string) {
960
    try {
1✔
961
      this.setCallControlBusyTimestamp();
1✔
962
      const session = this._getSessionById(telephonySessionId);
1✔
963
      const { webphoneSession, otherParties = [] } = session;
1!
964
      if (
1!
965
        // when call is connecting or in voicemail then call control's Hold API will not work
966
        // so use webphone hold here
967
        (session.direction === callDirection.outbound &&
4✔
968
          (otherParties[0]?.status?.code === PartyStatusCode.proceeding ||
969
            otherParties[0]?.status?.code === PartyStatusCode.voicemail)) ||
970
        isAtMainNumberPromptToneStage(session)
971
      ) {
972
        await webphoneSession.hold();
×
973
      } else {
974
        await session.hold();
1✔
975
      }
976
      if (webphoneSession && webphoneSession.__rc_callStatus) {
1!
977
        webphoneSession.__rc_callStatus = sessionStatus.onHold;
×
978
      }
979
      this.clearCallControlBusyTimestamp();
1✔
980
    } catch (error: any) {
981
      // TODO: fix error handling with instanceof
982
      if (error.response && !error.response._text) {
×
983
        error.response._text = await error.response.clone().text();
×
984
      }
985
      if (conflictError(error)) {
×
986
        this._deps.alert.warning({
×
987
          message: callControlError.holdConflictError,
988
        });
989
      } else if (
×
990
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
991
      ) {
992
        this._deps.alert.warning({ message: callControlError.generalError });
×
993
      }
994
      this.clearCallControlBusyTimestamp();
×
995
    }
996
  }
997

998
  @track((that: ActiveCallControl) => [
×
999
    that._getTrackEventName(trackEvents.unhold),
1000
  ])
1001
  @proxify
1002
  async unhold(telephonySessionId: string) {
1003
    try {
×
1004
      this.setCallControlBusyTimestamp();
×
1005
      const session = this._getSessionById(telephonySessionId);
×
1006
      await this._holdOtherCalls(telephonySessionId);
×
1007
      await session.unhold();
×
1008
      this._activeSession = session;
×
1009
      const { webphoneSession } = session;
×
1010
      if (webphoneSession && webphoneSession.__rc_callStatus) {
×
1011
        webphoneSession.__rc_callStatus = sessionStatus.connected;
×
1012
      }
1013
      this.setActiveSessionId(telephonySessionId);
×
1014
      this._addTrackToActiveSession();
×
1015
      this.clearCallControlBusyTimestamp();
×
1016
    } catch (error: any) {
1017
      // TODO: fix error handling with instanceof
1018
      if (error.response && !error.response._text) {
×
1019
        error.response._text = await error.response.clone().text();
×
1020
      }
1021
      if (conflictError(error)) {
×
1022
        this._deps.alert.warning({
×
1023
          message: callControlError.unHoldConflictError,
1024
        });
1025
      } else if (
×
1026
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
1027
      ) {
1028
        this._deps.alert.warning({
×
1029
          message: callControlError.generalError,
1030
        });
1031
      }
1032
      this.clearCallControlBusyTimestamp();
×
1033
    }
1034
  }
1035

1036
  @track((_, params: ReplyWithTextParams) => {
1037
    let messageType = 'End-User Custom Message';
×
1038
    if (params.replyWithPattern) {
×
1039
      const pattern = params.replyWithPattern?.pattern;
×
1040
      if (
×
1041
        pattern === ReplyWithPattern.inAMeeting ||
×
1042
        pattern === ReplyWithPattern.onMyWay
1043
      ) {
1044
        messageType = 'Default Static Message';
×
1045
      } else {
1046
        messageType = 'Default Dynamic Message';
×
1047
      }
1048
    }
1049
    return [
×
1050
      trackEvents.executionReplyWithMessage,
1051
      {
1052
        'Message Type': messageType,
1053
      },
1054
    ];
1055
  })
1056
  @proxify
1057
  async replyWithMessage(
1058
    params: ReplyWithTextParams,
1059
    telephonySessionId: string,
1060
  ) {
1061
    try {
×
1062
      this.setCallControlBusyTimestamp();
×
1063
      const session = this._getSessionById(telephonySessionId);
×
1064
      if (!session) {
×
1065
        return false;
×
1066
      }
1067
      // await session.replyWithMessage(params);
NEW
1068
      const webphoneReplyOption = getWebphoneReplyMessageOption(params) as any;
×
NEW
1069
      await session.webphoneSession.replyWithMessage(webphoneReplyOption);
×
1070
      this.clearCallControlBusyTimestamp();
×
1071
      this._deps.alert.success({ message: callControlError.replyCompleted });
×
1072
    } catch (error: any /** TODO: confirm with instanceof */) {
1073
      console.error('replyWithMessage error', error);
×
1074
      this._deps.alert.warning({ message: callControlError.generalError });
×
1075
      this.clearCallControlBusyTimestamp();
×
1076
    }
1077
  }
1078

1079
  @proxify
1080
  async toVoicemail(voicemailId: string, telephonySessionId: string) {
1081
    try {
×
1082
      this.setCallControlBusyTimestamp();
×
1083
      const session = this._getSessionById(telephonySessionId);
×
1084
      if (!session) {
×
1085
        return false;
×
1086
      }
1087
      await session.transfer(voicemailId, { type: 'voicemail' });
×
1088
      this.clearCallControlBusyTimestamp();
×
1089
      this._deps.alert.success({ message: callControlError.transferCompleted });
×
1090
    } catch (error: any /** TODO: confirm with instanceof */) {
1091
      console.error('toVoicemail error', error);
×
1092
      this._deps.alert.warning({ message: webphoneErrors.toVoiceMailError });
×
1093
      this.clearCallControlBusyTimestamp();
×
1094
    }
1095
  }
1096

1097
  @proxify
1098
  async completeWarmTransfer(telephonySession: string) {
1099
    try {
×
1100
      this.setCallControlBusyTimestamp();
×
1101

1102
      const { isOriginal, relatedTelephonySessionId } =
1103
        this.transferCallMapping[telephonySession];
×
1104

1105
      const session = this._getSessionById(
×
1106
        isOriginal ? telephonySession : relatedTelephonySessionId,
×
1107
      );
1108
      const transferSession = this._getSessionById(
×
1109
        isOriginal ? relatedTelephonySessionId : telephonySession,
×
1110
      );
1111

1112
      if (!transferSession) {
×
1113
        return false;
×
1114
      }
1115
      await session.warmTransfer(transferSession);
×
1116
      this.clearCallControlBusyTimestamp();
×
1117
      this._deps.alert.success({ message: callControlError.transferCompleted });
×
1118
    } catch (error: any /** TODO: confirm with instanceof */) {
1119
      console.error('warmTransfer error', error);
×
1120
      this._deps.alert.warning({ message: callControlError.generalError });
×
1121
      this.clearCallControlBusyTimestamp();
×
1122
    }
1123
  }
1124

1125
  @track(trackEvents.transfer)
1126
  @proxify
1127
  async transfer(transferNumber: string, telephonySessionId: string) {
1128
    try {
×
1129
      this.setCallControlBusyTimestamp();
×
1130
      const session = this._getSessionById(telephonySessionId);
×
1131
      const phoneNumber = await this.getValidPhoneNumber(transferNumber, true);
×
1132
      if (phoneNumber) {
×
1133
        await session.transfer(phoneNumber);
×
1134
        this.clearCallControlBusyTimestamp();
×
1135
        this._deps.alert.success({
×
1136
          message: callControlError.transferCompleted,
1137
        });
1138
      }
1139
    } catch (error: any) {
1140
      // TODO: fix error handling with instanceof
1141
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
1142
        this._deps.alert.warning({ message: webphoneErrors.transferError });
×
1143
      }
1144
      this.clearCallControlBusyTimestamp();
×
1145
    }
1146
  }
1147

1148
  async getValidPhoneNumber(phoneNumber: string, withMainNumber?: boolean) {
1149
    let validatedResult;
1150
    let validPhoneNumber;
1151
    if (!this._permissionCheck) {
3✔
1152
      validatedResult = validateNumbers({
1✔
1153
        allowRegionSettings: !!this._deps.brand.brandConfig.allowRegionSettings,
1154
        areaCode: this._deps.regionSettings.areaCode,
1155
        countryCode: this._deps.regionSettings.countryCode,
1156
        phoneNumbers: [phoneNumber],
1157
      });
1158
      validPhoneNumber = validatedResult[0];
1✔
1159
    } else {
1160
      const isEDPEnabled = this._deps.appFeatures?.isEDPEnabled;
2✔
1161
      validatedResult = isEDPEnabled
2!
1162
        ? this._deps.numberValidate.validate([phoneNumber])
1163
        : await this._deps.numberValidate.validateNumbers([phoneNumber]);
1164

1165
      if (!validatedResult.result) {
2✔
1166
        validatedResult.errors.forEach(async (error) => {
1✔
1167
          const isHAError =
1168
            // @ts-expect-error
1169
            !!(await this._deps.availabilityMonitor?.checkIfHAError(error));
1✔
1170
          if (!isHAError) {
1!
1171
            // TODO: fix `callErrors` type
1172
            this._deps.alert.warning({
1✔
1173
              message: (callErrors as any)[error.type],
1174
              payload: {
1175
                phoneNumber: error.phoneNumber,
1176
              },
1177
            });
1178
          }
1179
        });
1180
        return;
1✔
1181
      }
1182
      if (isEDPEnabled) {
1!
1183
        const parsedNumbers = await this._deps.numberValidate.parseNumbers([
×
1184
          phoneNumber,
1185
        ]);
1186
        validPhoneNumber =
×
1187
          parsedNumbers?.[0].availableExtension ??
×
1188
          parsedNumbers?.[0].parsedNumber;
1189
      } else {
1190
        // TODO: fix `validatedResult` type in `numberValidate` module.
1191
        validPhoneNumber = (validatedResult as any).numbers?.[0]?.e164;
1✔
1192
      }
1193
    }
1194

1195
    let result = validPhoneNumber;
2✔
1196
    if (withMainNumber && validPhoneNumber.indexOf('+') === -1) {
2!
1197
      result = [
×
1198
        this._deps.accountInfo.mainCompanyNumber,
1199
        validPhoneNumber,
1200
      ].join('*');
1201
    }
1202

1203
    return result;
2✔
1204
  }
1205

1206
  // FIXME: Incomplete Implementation?
1207
  @proxify
1208
  async flip(flipValue: string, telephonySessionId: string) {
1209
    try {
×
1210
      this.setCallControlBusyTimestamp();
×
1211
      const session = this._getSessionById(telephonySessionId);
×
1212
      await session.flip({ callFlipId: flipValue });
×
1213
      this.clearCallControlBusyTimestamp();
×
1214
    } catch (error: any /** TODO: confirm with instanceof */) {
1215
      console.error('flip error', error);
×
1216
      this.clearCallControlBusyTimestamp();
×
1217
      throw error;
×
1218
    }
1219
  }
1220

1221
  @track((that: ActiveCallControl) => [
×
1222
    that._getTrackEventName(trackEvents.confirmForward),
1223
  ])
1224
  @proxify
1225
  async forward(forwardNumber: string, telephonySessionId: string) {
1226
    const session = this._getSessionById(telephonySessionId);
2✔
1227
    if (!session) {
2!
1228
      return false;
×
1229
    }
1230
    try {
2✔
1231
      let validatedResult;
1232
      let validPhoneNumber;
1233
      if (!this._permissionCheck) {
2✔
1234
        validatedResult = validateNumbers({
1✔
1235
          allowRegionSettings:
1236
            !!this._deps.brand.brandConfig.allowRegionSettings,
1237
          areaCode: this._deps.regionSettings.areaCode,
1238
          countryCode: this._deps.regionSettings.countryCode,
1239
          phoneNumbers: [forwardNumber],
1240
        });
1241
        validPhoneNumber = validatedResult[0];
1✔
1242
      } else {
1243
        const isEDPEnabled = this._deps.appFeatures?.isEDPEnabled;
1✔
1244
        validatedResult = isEDPEnabled
1!
1245
          ? this._deps.numberValidate.validate([forwardNumber])
1246
          : await this._deps.numberValidate.validateNumbers([forwardNumber]);
1247

1248
        if (!validatedResult.result) {
1!
1249
          validatedResult.errors.forEach((error) => {
1✔
1250
            this._deps.alert.warning({
1✔
1251
              message: (callErrors as any)[error.type],
1252
              payload: {
1253
                phoneNumber: error.phoneNumber,
1254
              },
1255
            });
1256
          });
1257
          return false;
1✔
1258
        }
1259
        if (isEDPEnabled) {
×
1260
          const parsedNumbers = await this._deps.numberValidate.parseNumbers([
×
1261
            forwardNumber,
1262
          ]);
1263
          if (parsedNumbers) {
×
1264
            validPhoneNumber =
×
1265
              parsedNumbers[0].availableExtension ??
×
1266
              parsedNumbers[0].parsedNumber;
1267
          }
1268
        } else {
1269
          validPhoneNumber = (validatedResult as any).numbers?.[0]?.e164;
×
1270
        }
1271
      }
1272
      if (session && session.webphoneSession) {
1!
1273
        session.webphoneSession.__rc_isForwarded = true;
×
1274
      }
1275

1276
      await session.forward(validPhoneNumber, this.acceptOptions);
1✔
1277
      this._deps.alert.success({
×
1278
        message: callControlError.forwardSuccess,
1279
      });
1280
      if (typeof this._onCallEndFunc === 'function') {
×
1281
        this._onCallEndFunc();
×
1282
      }
1283
      return true;
×
1284
    } catch (e: any /** TODO: confirm with instanceof */) {
1285
      console.error(e);
1✔
1286
      this._deps.alert.warning({
1✔
1287
        message: webphoneErrors.forwardError,
1288
      });
1289
      return false;
1✔
1290
    }
1291
  }
1292

1293
  // DTMF handing by webphone session temporary, due to rc call session doesn't support currently
1294
  @proxify
1295
  async sendDTMF(dtmfValue: string, telephonySessionId: string) {
1296
    try {
×
1297
      const session = this._getSessionById(telephonySessionId);
×
1298
      // TODO: using rc call session
1299
      const { webphoneSession } = session;
×
1300
      if (webphoneSession) {
×
1301
        await webphoneSession.dtmf(dtmfValue, 100);
×
1302
      }
1303
    } catch (error: any /** TODO: confirm with instanceof */) {
1304
      console.log('send dtmf error', error);
×
1305
      throw error;
×
1306
    }
1307
  }
1308

1309
  private _onWebphoneInvite(session: WebphoneSession) {
1310
    const webphoneSession = session;
13✔
1311
    if (!webphoneSession) return;
13!
1312
    if (!webphoneSession.__rc_creationTime) {
13!
1313
      webphoneSession.__rc_creationTime = Date.now();
×
1314
    }
1315
    if (!webphoneSession.__rc_lastActiveTime) {
13!
1316
      webphoneSession.__rc_lastActiveTime = Date.now();
×
1317
    }
1318
    webphoneSession.on('terminated', () => {
13✔
1319
      console.log('Call Event: terminated');
1✔
1320
      // this.setLastEndedSessionIds(webphoneSession);
1321
      const { telephonySessionId } =
1322
        this.rcCallSessions.find(
1!
1323
          (s) => s.webphoneSession === webphoneSession,
2✔
1324
        ) || {};
1325

1326
      if (!telephonySessionId) return;
1!
1327

1328
      this._setActiveSessionIdFromOnHoldCalls(telephonySessionId);
×
1329
      this._onCallEnd(telephonySessionId);
×
1330
    });
1331
    webphoneSession.on('accepted', () => {
13✔
1332
      const { telephonySessionId } =
1333
        this.rcCallSessions.find(
11!
1334
          (s) => s.webphoneSession === webphoneSession,
26✔
1335
        ) || {};
1336

1337
      if (!telephonySessionId) return;
11!
1338

1339
      if (this._autoMergeWebphoneSessionsMap.get(webphoneSession)) {
×
1340
        this._autoMergeWebphoneSessionsMap.delete(webphoneSession);
×
1341
      } else {
1342
        this.setActiveSessionId(telephonySessionId);
×
1343
        this._holdOtherCalls(telephonySessionId);
×
1344
        this._addTrackToActiveSession();
×
1345
      }
1346
      this.updateActiveSessions();
×
1347
      this._onCallAccepted(telephonySessionId);
×
1348
    });
1349
  }
1350

1351
  @action
1352
  private _setRingSessionId(sessionId: string | null = null) {
×
1353
    this.data.ringSessionId = sessionId;
13✔
1354
  }
1355

1356
  /**
1357
   *if current call is terminated, then pick the first onhold call as active current call;
1358
   *
1359
   * @param {Session} session
1360
   * @memberof ActiveCallControl
1361
   */
1362
  private _setActiveSessionIdFromOnHoldCalls(telephonySessionId: string) {
1363
    if (!telephonySessionId) return;
×
1364
    if (this.activeSessionId === telephonySessionId) {
×
1365
      const onHoldSessions: ActiveCallControlSessionData[] = filter(
×
1366
        (s: ActiveCallControlSessionData) => isHolding(s),
×
1367
        this.sessions,
1368
      );
1369
      if (onHoldSessions.length) {
×
1370
        this.setActiveSessionId(onHoldSessions[0].telephonySessionId);
×
1371
      }
1372
    }
1373
  }
1374

1375
  @proxify
1376
  private async _holdOtherCalls(telephonySessionId?: string) {
1377
    const currSessions = this._rcCall!.sessions! as Session[];
1✔
1378
    const otherSessions = filter((s) => {
1✔
1379
      return (
2✔
1380
        s.telephonySessionId !== telephonySessionId &&
3!
1381
        (s.status === PartyStatusCode.answered ||
1382
          (s.webphoneSession && !s.webphoneSession.localHold))
1383
      );
1384
    }, currSessions);
1385
    if (!otherSessions.length) {
1!
1386
      return;
×
1387
    }
1388
    const holdOtherSessions = otherSessions.map(async (session) => {
1✔
1389
      const { webphoneSession, otherParties = [] } = session;
1!
1390
      try {
1✔
1391
        if (
1!
1392
          // when call is connecting or in voicemail then call control's Hold API will not work
1393
          // so use webphone hold here
1394
          (session.direction === callDirection.outbound &&
4✔
1395
            (otherParties[0]?.status?.code === PartyStatusCode.proceeding ||
1396
              otherParties[0]?.status?.code === PartyStatusCode.voicemail)) ||
1397
          isAtMainNumberPromptToneStage(session)
1398
        ) {
1399
          await webphoneSession.hold();
1✔
1400
        } else {
1401
          await session.hold();
×
1402
        }
1403
        if (webphoneSession && webphoneSession.__rc_callStatus) {
1!
1404
          webphoneSession.__rc_callStatus = sessionStatus.onHold;
×
1405
        }
1406
      } catch (error: any /** TODO: confirm with instanceof */) {
1407
        console.log('Hold call fail.', error);
×
1408
      }
1409
    });
1410
    await Promise.all(holdOtherSessions);
1✔
1411
  }
1412

1413
  @action
1414
  setPickUpCallData(data: IPickUpCallDataMap) {
NEW
1415
    this.pickUpCallDataMap = { ...data };
×
1416
  }
1417

1418
  @proxify
1419
  private async _answer(telephonySessionId: string) {
NEW
1420
    try {
×
NEW
1421
      this._triggerAutoMergeEvent(telephonySessionId);
×
NEW
1422
      this.setCallControlBusyTimestamp();
×
NEW
1423
      const session = this._getSessionById(telephonySessionId);
×
1424

NEW
1425
      this._activeSession = session;
×
NEW
1426
      await this._holdOtherCalls(telephonySessionId);
×
NEW
1427
      const { webphoneSession } = session;
×
NEW
1428
      const deviceId = this._deps.webphone?.device?.id;
×
NEW
1429
      if (webphoneSession) {
×
NEW
1430
        await session.answer({ deviceId });
×
1431
      } else {
NEW
1432
        await this.pickUpCall({
×
1433
          ...this.pickUpCallDataMap[telephonySessionId],
1434
        });
1435
      }
NEW
1436
      this._trackWebRTCCallAnswer();
×
NEW
1437
      if (webphoneSession && webphoneSession.__rc_callStatus) {
×
NEW
1438
        webphoneSession.__rc_callStatus = sessionStatus.connected;
×
1439
      }
1440
    } finally {
NEW
1441
      this.clearCallControlBusyTimestamp();
×
1442
    }
1443
  }
1444

1445
  public async pickUpCall(data: IPickUpCallParams) {
NEW
1446
    const { telephonySessionId } = data;
×
NEW
1447
    await this._rcCall?.pickupInboundCall({
×
1448
      ...this.pickUpCallDataMap[telephonySessionId],
1449
      ...data,
1450
      ...this.acceptOptions,
1451
    });
1452
  }
1453

1454
  @track((that: ActiveCallControl) => [
×
1455
    that._getTrackEventName(trackEvents.answer),
1456
  ])
1457
  @proxify
1458
  async answer(telephonySessionId: string) {
1459
    try {
×
1460
      await this._answer(telephonySessionId);
×
1461
    } catch (error: any /** TODO: confirm with instanceof */) {
1462
      console.log('answer failed.');
×
1463
    }
1464
  }
1465

1466
  @track((that: ActiveCallControl) => [
×
1467
    that._getTrackEventName(trackEvents.holdAndAnswer),
1468
  ])
1469
  @proxify
1470
  async answerAndHold(telephonySessionId: string) {
1471
    // currently, the logic is same as answer
1472
    try {
×
1473
      await this._answer(telephonySessionId);
×
1474
    } catch (error: any /** TODO: confirm with instanceof */) {
1475
      console.log('answer hold failed.', error);
×
1476
    }
1477
  }
1478

1479
  /**
1480
   * ignore an incoming WebRTC call, after action executed, call will be ignored at current
1481
   * device and move to "calls on other device" section. This call still can be answered at other
1482
   * device
1483
   * @param {string} telephonySessionId
1484
   * @memberof ActiveCallControl
1485
   */
1486
  @track((that: ActiveCallControl) => [
×
1487
    that._getTrackEventName(trackEvents.ignore),
1488
  ])
1489
  @proxify
1490
  async ignore(telephonySessionId: string) {
1491
    try {
×
1492
      this.setCallControlBusyTimestamp();
×
1493
      const session = this._getSessionById(telephonySessionId);
×
1494
      const { webphoneSession } = session;
×
NEW
1495
      await webphoneSession?.reject();
×
1496
      // hack for update sessions, then incoming call log page can re-render
1497
      setTimeout(() => this.updateActiveSessions(), 0);
×
1498
      this.clearCallControlBusyTimestamp();
×
NEW
1499
      this.onCallIgnoreFunc?.(session.party.id);
×
1500
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1501
      console.log('===ignore failed.', error);
×
1502
    }
1503
  }
1504

1505
  @track((that: ActiveCallControl) => [
×
1506
    that._getTrackEventName(trackEvents.endAndAnswer),
1507
  ])
1508
  @proxify
1509
  async answerAndEnd(telephonySessionId: string) {
1510
    try {
1✔
1511
      if (this.busy) return;
1!
1512
      this.setCallControlBusyTimestamp();
1✔
1513
      const session = this._getSessionById(telephonySessionId);
1✔
1514
      const currentActiveCalls = this._rcCall!.sessions.filter(
1✔
1515
        (s) =>
1516
          s.id !== telephonySessionId &&
2✔
1517
          s.webphoneSession &&
1518
          (s.status === PartyStatusCode.answered ||
1519
            (s.direction === callDirection.outbound &&
1520
              s.status === PartyStatusCode.proceeding)),
1521
      );
1522
      for (const s of currentActiveCalls) {
1✔
1523
        await s.hangup();
1✔
1524
      }
1525
      const deviceId = this._deps.webphone?.device?.id;
1✔
NEW
1526
      if (session.webphoneSession) {
×
NEW
1527
        await session.answer({ deviceId });
×
1528
      } else {
NEW
1529
        await this.pickUpCall({
×
1530
          ...this.pickUpCallDataMap[telephonySessionId],
1531
        });
1532
      }
1533
      this._trackWebRTCCallAnswer();
×
1534
      const { webphoneSession } = session;
×
1535
      if (webphoneSession && webphoneSession.__rc_callStatus) {
×
1536
        webphoneSession.__rc_callStatus = sessionStatus.connected;
×
1537
      }
1538
      this.clearCallControlBusyTimestamp();
×
1539
    } catch (error: any /** TODO: confirm with instanceof */) {
1540
      console.log('answer and end fail.');
1✔
1541
      console.error(error);
1✔
1542
    }
1543
  }
1544

1545
  async startWarmTransfer(transferNumber: string, telephonySessionId: string) {
1546
    // todo handle error;
1547
    const toNumber = await this.getValidPhoneNumber(transferNumber);
×
1548
    return this.makeCall({
×
1549
      toNumber,
1550
      transferSessionId: telephonySessionId,
1551
    });
1552
  }
1553

1554
  @action
1555
  setWarmTransferMapping(originalId: string, transferredId: string) {
1556
    this.transferCallMapping = {
×
1557
      ...this.transferCallMapping,
1558
      [originalId]: {
1559
        relatedTelephonySessionId: transferredId,
1560
        isOriginal: true,
1561
      },
1562
      [transferredId]: {
1563
        relatedTelephonySessionId: originalId,
1564
        isOriginal: false,
1565
      },
1566
    };
1567
  }
1568

1569
  @action
1570
  cleanCurrentWarmTransferData() {
1571
    const warmTransferSessionIds = Object.keys(this.transferCallMapping);
×
1572
    const currentSessionIds = this.sessions.map(
×
1573
      (session) => session.telephonySessionId,
×
1574
    );
1575
    const needRemovedIds = warmTransferSessionIds.filter(
×
1576
      (telephonySessionId) => !currentSessionIds.includes(telephonySessionId),
×
1577
    );
1578

1579
    if (needRemovedIds.length > 0) {
×
1580
      const removeSessionSet = new Set(needRemovedIds);
×
1581

1582
      const filteredData = Object.fromEntries(
×
1583
        Object.entries(this.transferCallMapping).filter(
1584
          ([id, transferInfo]) =>
1585
            !(
×
1586
              removeSessionSet.has(id) ||
×
1587
              removeSessionSet.has(transferInfo.relatedTelephonySessionId)
1588
            ),
1589
        ),
1590
      );
1591

1592
      this.transferCallMapping = filteredData;
×
1593
    }
1594
  }
1595

1596
  @proxify
1597
  async makeCall(params: ModuleMakeCallParams) {
1598
    try {
×
1599
      if (
×
1600
        params.toNumber.length > 6 &&
×
1601
        (!this._deps.availabilityMonitor ||
1602
          !this._deps.availabilityMonitor.isVoIPOnlyMode)
1603
      ) {
1604
        const phoneLines = await this._fetchDL();
×
1605
        if (phoneLines.length === 0) {
×
1606
          this._deps.alert.warning({
×
1607
            message: webphoneErrors.noOutboundCallWithoutDL,
1608
          });
1609
          return null;
×
1610
        }
1611
      }
1612
      await this._holdOtherCalls();
×
1613
      const sdkMakeCallParams: MakeCallParams = {
×
1614
        // type 'callControl' not support webphone's sip device currently.
1615
        type: 'webphone',
1616
        toNumber: params.toNumber,
1617
        fromNumber: params.fromNumber,
1618
        homeCountryId: params.homeCountryId,
1619
      };
1620
      const session = (await this._rcCall!.makeCall(
×
1621
        sdkMakeCallParams,
1622
      )) as Session;
1623
      this._activeSession = session;
×
1624
      session.webphoneSession.on('progress', () => {
×
1625
        if (
×
1626
          session.telephonySessionId &&
×
1627
          this.activeSessionId !== session.telephonySessionId
1628
        ) {
1629
          this.setActiveSessionId(session.telephonySessionId);
×
1630

1631
          const { transferSessionId } = params;
×
1632
          if (transferSessionId) {
×
1633
            this.setWarmTransferMapping(
×
1634
              transferSessionId,
1635
              session.telephonySessionId,
1636
            );
1637
          }
1638
        }
1639
      });
1640
      this._triggerAutoMergeEvent();
×
1641
      return session;
×
1642
    } catch (error: any /** TODO: confirm with instanceof */) {
1643
      console.log('make call fail.', error);
×
1644
    }
1645
  }
1646

1647
  private async _fetchDL() {
1648
    const response = await this._deps.client
×
1649
      .account()
1650
      .extension()
1651
      .device()
1652
      .list();
1653
    const devices = response.records;
×
1654
    let phoneLines: any[] = [];
×
1655
    devices?.forEach((device) => {
×
1656
      // wrong type of phoneLines, temporary treat it as any
1657
      if (!device.phoneLines || (device.phoneLines as any).length === 0) {
×
1658
        return;
×
1659
      }
1660
      phoneLines = phoneLines.concat(device.phoneLines);
×
1661
    });
1662
    return phoneLines;
×
1663
  }
1664

1665
  getActiveSession(telephonySessionId: string | null) {
1666
    if (!telephonySessionId) {
6!
1667
      return null;
6✔
1668
    }
1669
    return this.activeSessions[telephonySessionId];
×
1670
  }
1671

1672
  getSession(telephonySessionId: string) {
1673
    return this.sessions.find(
×
1674
      (session) => session.telephonySessionId === telephonySessionId,
×
1675
    );
1676
  }
1677

1678
  @computed(({ activeSessionId, activeSessions }: ActiveCallControl) => [
68✔
1679
    activeSessionId,
1680
    activeSessions,
1681
  ])
1682
  get activeSession() {
1683
    return this.getActiveSession(this.activeSessionId);
6✔
1684
  }
1685

1686
  @computed(({ ringSessionId, activeSessions }: ActiveCallControl) => [
×
1687
    ringSessionId,
1688
    activeSessions,
1689
  ])
1690
  get ringSession() {
1691
    return this.getActiveSession(this.ringSessionId);
×
1692
  }
1693

1694
  @computed(({ sessions }: ActiveCallControl) => [sessions])
×
1695
  get ringSessions() {
1696
    if (!this.sessions) {
×
1697
      return [];
×
1698
    }
1699
    return this.sessions.filter((session: ActiveCallControlSessionData) =>
×
1700
      isRinging(session),
×
1701
    );
1702
  }
1703

1704
  @computed((that: ActiveCallControl) => [that.sessions, that.timestamp])
68✔
1705
  get activeSessions() {
1706
    return this.sessions.reduce((accumulator, session) => {
6✔
1707
      const { id } = session;
×
1708
      accumulator[id!] = normalizeSession({ session });
×
1709
      return accumulator;
×
1710
    }, {} as Record<string, Partial<ActiveSession>>);
1711
  }
1712

1713
  @computed((that: ActiveCallControl) => [that._deps.presence.calls])
×
1714
  get sessionIdToTelephonySessionIdMapping() {
1715
    return this._deps.presence.calls.reduce((accumulator, call) => {
×
1716
      const { telephonySessionId, sessionId } = call;
×
1717
      accumulator[sessionId!] = telephonySessionId!;
×
1718
      return accumulator;
×
1719
    }, {} as Record<string, string>);
1720
  }
1721

1722
  /**
1723
   * Mitigation strategy for avoiding 404/409 on call control endpoints.
1724
   * This should gradually move towards per session controls rather than
1725
   * a global busy timeout.
1726
   */
1727
  get busy() {
1728
    return Date.now() - this.busyTimestamp < DEFAULT_BUSY_TIMEOUT;
253✔
1729
  }
1730

1731
  // This should reflect on the app permissions setting in DWP
1732
  get hasPermission() {
1733
    return this._deps.appFeatures.hasCallControl;
20,393✔
1734
  }
1735

1736
  get timeToRetry() {
1737
    return this._timeToRetry;
×
1738
  }
1739

1740
  get ttl() {
1741
    return this._ttl;
×
1742
  }
1743

1744
  get acceptOptions() {
1745
    return {
×
1746
      sessionDescriptionHandlerOptions: {
1747
        constraints: {
1748
          audio: {
1749
            deviceId: this._deps.audioSettings?.inputDeviceId,
1750
          },
1751
          video: false,
1752
        },
1753
      },
1754
    };
1755
  }
1756

1757
  get hasCallInRecording() {
1758
    return this.sessions.some((session) => isRecording(session));
×
1759
  }
1760

1761
  // TODO:refactor, use this.sessions instead
1762
  get rcCallSessions() {
1763
    return filter(
12✔
1764
      (session) => filterDisconnectedCalls(session),
30✔
1765
      this._rcCall?.sessions || [],
12!
1766
    );
1767
  }
1768

1769
  get activeSessionId() {
1770
    return this.data.activeSessionId;
75,562✔
1771
  }
1772

1773
  get busyTimestamp() {
1774
    return this.data.busyTimestamp;
253✔
1775
  }
1776

1777
  get timestamp() {
1778
    return this.data.timestamp;
68✔
1779
  }
1780

1781
  get sessions() {
1782
    return this.data.sessions;
49,867✔
1783
  }
1784

1785
  get ringSessionId() {
1786
    return this.data.ringSessionId;
×
1787
  }
1788

1789
  @track(trackEvents.inboundWebRTCCallConnected)
1790
  _trackWebRTCCallAnswer() {
1791
    //
1792
  }
1793

1794
  @track(trackEvents.dialpadOpen)
1795
  dialpadOpenTrack() {
1796
    //
1797
  }
1798

1799
  @track(trackEvents.dialpadClose)
1800
  dialpadCloseTrack() {
1801
    //
1802
  }
1803

1804
  @track((that: ActiveCallControl) => [
×
1805
    that._getTrackEventName(trackEvents.clickTransfer),
1806
  ])
1807
  clickTransferTrack() {
1808
    //
1809
  }
1810

1811
  @track((that: ActiveCallControl) => [
×
1812
    that._getTrackEventName(trackEvents.forward),
1813
  ])
1814
  clickForwardTrack() {
1815
    //
1816
  }
1817

1818
  @track((that: ActiveCallControl, path: string) => {
1819
    return (analytics) => {
×
1820
      // @ts-expect-error
1821
      const target = analytics.getTrackTarget();
×
1822
      return [
×
1823
        trackEvents.openEntityDetailLink,
1824
        { path: path || target.router },
×
1825
      ];
1826
    };
1827
  })
1828
  openEntityDetailLinkTrack(path: string) {
1829
    //
1830
  }
1831

1832
  @track((that: ActiveCallControl) => [
×
1833
    that._getTrackEventName(trackEvents.switch),
1834
  ])
1835
  clickSwitchTrack() {
1836
    //
1837
  }
1838

1839
  private _getSessionById(sessionId: string) {
1840
    const session = this._rcCall!.sessions.find((s) => s.id === sessionId);
4✔
1841

1842
    return session as Session;
4✔
1843
  }
1844
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc