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

ringcentral / ringcentral-js-widgets / 9984535799

18 Jul 2024 02:28AM UTC coverage: 63.055% (+1.7%) from 61.322%
9984535799

push

github

web-flow
misc: sync features and bugfixes from f39b7a45 (#1747)

* misc: sync features and bugfixes from ba8d789

* misc: add i18n-dayjs and react-hooks packages

* version to 0.15.0

* chore: update crius

* misc: sync from f39b7a45

* chore: fix tests

* chore: fix tests

* chore: run test with --updateSnapshot

9782 of 17002 branches covered (57.53%)

Branch coverage included in aggregate %.

2206 of 3368 new or added lines in 501 files covered. (65.5%)

219 existing lines in 52 files now uncovered.

16601 of 24839 relevant lines covered (66.83%)

178566.16 hits per line

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

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

44
import type {
45
  ActiveCallControlSessionData,
46
  ActiveSession,
47
  Deps,
48
  ICurrentDeviceCallsMap,
49
  ITransferCallSessionMapping,
50
  ModuleMakeCallParams,
51
  IPickUpCallDataMap,
52
  IPickUpCallParams,
53
} from './ActiveCallControl.interface';
54
import { callControlError } from './callControlError';
55
import {
56
  conflictError,
57
  filterDisconnectedCalls,
58
  isAtMainNumberPromptToneStage,
59
  isHolding,
60
  isOnRecording,
61
  isRecording,
62
  isRinging,
63
  normalizeSession,
64
  normalizeTelephonySession,
65
  getWebphoneReplyMessageOption,
66
  isGoneSession,
67
} from './helpers';
68

69
const DEFAULT_TTL = 30 * 60 * 1000;
203✔
70
const DEFAULT_TIME_TO_RETRY = 62 * 1000;
203✔
71
const DEFAULT_BUSY_TIMEOUT = 3 * 1000;
203✔
72
const telephonySessionsEndPoint = /\/telephony\/sessions$/;
203✔
73
const subscribeEvent = subscriptionFilters.telephonySessions;
203✔
74

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

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

123
  @state
124
  pickUpCallDataMap: IPickUpCallDataMap = {};
345✔
125

126
  constructor(deps: Deps) {
127
    super({
345✔
128
      deps,
129
      enableCache: deps.activeCallControlOptions?.enableCache ?? true,
690✔
130
      storageKey: 'activeCallControl',
131
    });
132
  }
133

134
  override async onStateChange() {
135
    if (this.ready && this.hasPermission) {
182,840✔
136
      this._subscriptionHandler();
25,678✔
137
      this._checkConnectivity();
25,678✔
138
    }
139
  }
140

141
  override async _initModule() {
142
    this._createOtherInstanceListener();
342✔
143
    await super._initModule();
342✔
144
  }
145

146
  _createOtherInstanceListener() {
147
    if (!this._deps.tabManager || !this._enableAutoSwitchFeature) {
342!
148
      return;
342✔
149
    }
150
    window.addEventListener('storage', (e) => {
×
151
      this._onStorageChangeEvent(e);
×
152
    });
153
  }
154

155
  _onStorageChangeEvent(e: StorageEvent) {
156
    switch (e.key) {
×
157
      case this._autoMergeSignCallIdKey:
158
        this._triggerCurrentClientAutoMerge(e);
×
159
        break;
×
160
      case this._autoMergeCallsKey:
161
        this._autoMergeCallsHandler(e);
×
162
        break;
×
163
      default:
164
        break;
×
165
    }
166
  }
167

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

191
  async _autoMergeCallsHandler(e: StorageEvent) {
192
    if (!this._deps.tabManager?.active) return;
×
193

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

217
          if (!activeCall) {
×
218
            console.log(
×
219
              `Auto Switch failed with telephonySessionId ${telephonySessionId}`,
220
            );
221
            return acc;
×
222
          }
223

224
          acc.push(activeCall);
×
225
          return acc;
×
226
        }, [] as ActiveCallInfo[]);
227

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

257
  _triggerAutoMergeEvent(telephoneSessionId?: string) {
258
    if (!this._deps.tabManager || !this._enableAutoSwitchFeature) return;
×
259

260
    const id = uuidV4();
×
261
    const data = {
×
262
      id,
263
      telephoneSessionId,
264
    };
265
    localStorage.setItem(this._autoMergeSignCallIdKey, JSON.stringify(data));
×
266
  }
267

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

284
  @storage
285
  @state
286
  transferCallMapping: ITransferCallSessionMapping = {};
345✔
287

288
  @storage
289
  @state
290
  data: {
291
    activeSessionId: string | null;
292
    busyTimestamp: number;
293
    timestamp: number;
294
    sessions: ActiveCallControlSessionData[];
295
    ringSessionId: string | null;
296
  } = {
345✔
297
    activeSessionId: null,
298
    busyTimestamp: 0,
299
    timestamp: 0,
300
    sessions: [],
301
    ringSessionId: null,
302
  };
303

304
  @state
305
  currentDeviceCallsMap: ICurrentDeviceCallsMap = {};
345✔
306

307
  @state
308
  lastEndedSessionIds: string[] = [];
345✔
309

310
  // TODO: conference call using
311
  @state
312
  cachedSessions: object[] = [];
345✔
313

314
  override async onInit() {
315
    if (!this.hasPermission) return;
346✔
316

317
    await this._deps.subscription.subscribe([subscribeEvent]);
343✔
318
    this._rcCall = this._initRcCall();
343✔
319

320
    if (this._shouldFetch()) {
343!
321
      try {
343✔
322
        await this.fetchData();
343✔
323
      } catch (e: any /** TODO: confirm with instanceof */) {
324
        this._retry();
×
325
      }
326
    } else if (this._polling) {
×
327
      this._startPolling();
×
328
    } else {
329
      this._retry();
×
330
    }
331
  }
332

333
  private _initRcCall() {
334
    const rcCall = new RingCentralCall({
343✔
335
      sdk: this._deps.client.service,
336
      subscriptions: undefined,
337
      enableSubscriptionHander: false,
338
      callControlOptions: {
339
        preloadDevices: false,
340
        preloadSessions: false,
341
        extensionInfo: {
342
          ...this._deps.extensionInfo.info,
343
          // TODO: add info type in 'AccountInfo'
344
          // @ts-expect-error TS(2322): Type 'GetAccountInfoResponse' is not assignable to... Remove this comment to see the full error message
345
          account: this._deps.accountInfo.info,
346
        },
347
      },
348
      webphone: this._deps.webphone?._webphone!,
349
    });
350
    rcCall.on(callEvents.NEW, (session: Session) => {
343✔
351
      this._newSessionHandler(session);
44✔
352
    });
353
    rcCall.on(callEvents.WEBPHONE_INVITE, (session: WebphoneSession) =>
343✔
354
      this._onWebphoneInvite(session),
23✔
355
    );
356
    rcCall.on(callEvents.WEBPHONE_INVITE_SENT, (session: WebphoneSession) =>
343✔
357
      this._onWebphoneInvite(session),
×
358
    );
359
    // TODO: workaround of bug:
360
    // WebRTC outbound call with wrong sequences of telephony sessions then call log section will not show
361
    // @ts-expect-error TS(2341): Property '_callControl' is private and only access... Remove this comment to see the full error message
362
    rcCall._callControl?.on('new', (session: Session) =>
343✔
363
      this._onNewCall(session),
24✔
364
    );
365

366
    return rcCall;
343✔
367
  }
368

369
  override onInitOnce() {
370
    if (this._deps.availabilityMonitor && this._deps.tabManager) {
342!
371
      watch(
342✔
372
        this,
373
        () => this.currentDeviceCallsMap,
99,959✔
374
        () => {
375
          const hasCallSession = Object.values(this.currentDeviceCallsMap).some(
775✔
376
            (webphoneSession) => !!webphoneSession,
105✔
377
          );
378
          const key = `acc-${this._deps.tabManager!.id}`;
775✔
379
          this._deps.availabilityMonitor!.setSharedState(key, {
775✔
380
            hasCallSession,
381
          });
382
        },
383
      );
384
    }
385
    if (this._deps.webphone) {
342!
386
      watch(
342✔
387
        this,
388
        () => this._deps.webphone?.connected,
99,959✔
389
        (newValue) => {
390
          if (newValue && this._deps.webphone?._webphone) {
660✔
391
            this._rcCall?.setWebphone(this._deps.webphone._webphone);
345✔
392
          }
393
        },
394
      );
395

396
      watch(
342✔
397
        this,
398
        () => this.activeSessionId,
99,959✔
399
        () => {
400
          this._addTrackToActiveSession();
×
401
        },
402
      );
403
    }
404
  }
405

406
  override onReset() {
407
    this.resetState();
314✔
408
  }
409

410
  @action
411
  resetState() {
412
    this.data.activeSessionId = null;
314✔
413
    this.data.busyTimestamp = 0;
314✔
414
    this.data.timestamp = 0;
314✔
415
    this.data.sessions = [];
314✔
416
  }
417

418
  _shouldFetch() {
419
    return !this._deps.tabManager || this._deps.tabManager.active;
343✔
420
  }
421

422
  @proxify
423
  async fetchData() {
424
    if (!this._promise) {
702!
425
      this._promise = this._fetchData();
702✔
426
    }
427
    await this._promise;
702✔
428
  }
429

430
  _clearTimeout() {
431
    if (this._timeoutId) clearTimeout(this._timeoutId);
×
432
  }
433

434
  _subscriptionHandler() {
435
    let { message } = this._deps.subscription;
25,678✔
436
    if (
25,678✔
437
      message &&
30,701✔
438
      // FIXME: is that object compare is fine, should confirm that?
439
      message !== this._lastSubscriptionMessage &&
440
      message.event &&
441
      telephonySessionsEndPoint.test(message.event) &&
442
      message.body
443
    ) {
444
      message = this._checkRingOutCallDirection(message);
24✔
445
      this._lastSubscriptionMessage = message;
24✔
446
      if (this._rcCall) {
24!
447
        this._rcCall.onNotificationEvent(message);
24✔
448
      }
449
    }
450
  }
451

452
  // TODO: workaround of PLA bug: https://jira_domain/browse/PLA-52742, remove these code after PLA
453
  // fixed this bug
454
  private _checkRingOutCallDirection(message: ExtensionTelephonySessionsEvent) {
455
    const { body } = message;
24✔
456
    const originType = body?.origin?.type;
24✔
457

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

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

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

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

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

544
  private _onSessionDisconnected = () => {
345✔
545
    this.updateActiveSessions();
2✔
546
    if (!this._deps.tabManager || this._deps.tabManager?.active) {
2!
547
      this.cleanCurrentWarmTransferData();
2✔
548
    }
549
  };
550

551
  private _updateSessionsStatusHandler = ({
345✔
552
    status,
553
    telephonySessionId,
554
  }: {
555
    status: PartyStatusCode;
556
    telephonySessionId: string;
557
  }) => {
558
    this.updateActiveSessions();
×
559

560
    if (
×
561
      status === PartyStatusCode.answered &&
×
562
      this.activeSessionId !== telephonySessionId
563
    ) {
564
      this.setActiveSessionId(telephonySessionId);
×
565
    }
566
  };
567

568
  private _updateSessionsHandler = () => {
345✔
569
    this.updateActiveSessions();
47✔
570
  };
571

572
  @proxify
573
  async _onNewCall(session: Session) {
574
    this.updateActiveSessions();
24✔
575
    const ringSession = find(
24✔
576
      (x) => isRinging(x) && x.id === session.id,
34✔
577
      this.sessions,
578
    );
579
    const sessionId = ringSession?.id;
24✔
580

581
    this._setRingSessionId(sessionId);
24✔
582
  }
583

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

591
  @action
592
  private _onCallEnd(telephonySessionId: string) {
593
    if (this.ringSessionId === telephonySessionId) {
×
594
      this.data.ringSessionId = this.ringSessions[0]?.id || null;
×
595
    }
596
  }
597

598
  updateActiveSessions() {
599
    const currentDeviceCallsMap: ICurrentDeviceCallsMap = {};
776✔
600
    const callControlSessions = (this._rcCall?.sessions || [])
776!
601
      .filter((session) => filterDisconnectedCalls(session))
126✔
602
      .map((session) => {
603
        // @ts-expect-error TS(2322): Type 'NormalizedSession | undefined' is not assign... Remove this comment to see the full error message
604
        currentDeviceCallsMap[session.telephonySessionId] =
126✔
605
          // @ts-expect-error TS(2345): Argument of type 'WebPhoneSession' is not assignab... Remove this comment to see the full error message
606
          normalizeWebphoneSession(session.webphoneSession);
607

608
        return {
126✔
609
          ...session.data,
610
          activeCallId: session.activeCallId,
611
          direction: session.direction,
612
          from: session.from,
613
          id: session.id,
614
          otherParties: session.otherParties,
615
          party: session.party || {},
162✔
616
          recordings: session.recordings,
617
          isRecording: isOnRecording(session.recordings),
618
          sessionId: session.sessionId,
619
          startTime: session.startTime,
620
          status: session.status,
621
          telephonySessionId: session.telephonySessionId,
622
          telephonySession: normalizeTelephonySession(session.telephonySession),
623
          to: session.to,
624
        } as ActiveCallControlSessionData;
625
      });
626
    this._updateActiveSessions(
776✔
627
      currentDeviceCallsMap,
628
      callControlSessions.filter((x) => !isGoneSession(x)),
126✔
629
    );
630
  }
631

632
  @action
633
  private _updateActiveSessions(
634
    currentDeviceCallsMap: ICurrentDeviceCallsMap,
635
    callControlSessions: ActiveCallControlSessionData[],
636
  ) {
637
    this.data.timestamp = Date.now();
776✔
638
    this.currentDeviceCallsMap = currentDeviceCallsMap;
776✔
639
    this.data.sessions = callControlSessions || [];
776!
640
  }
641

642
  private _newSessionHandler(session: Session) {
643
    session.removeListener(
44✔
644
      eventsEnum.STATUS,
645
      this._updateSessionsStatusHandler,
646
    );
647
    session.removeListener(eventsEnum.MUTED, this._updateSessionsHandler);
44✔
648
    session.removeListener(eventsEnum.RECORDINGS, this._updateSessionsHandler);
44✔
649
    session.removeListener(
44✔
650
      eventsEnum.DISCONNECTED,
651
      this._onSessionDisconnected,
652
    );
653
    session.removeListener(
44✔
654
      eventsEnum.WEBPHONE_SESSION_CONNECTED,
655
      this._updateSessionsHandler,
656
    );
657
    session.on(eventsEnum.STATUS, this._updateSessionsStatusHandler);
44✔
658
    session.on(eventsEnum.MUTED, this._updateSessionsHandler);
44✔
659
    session.on(eventsEnum.RECORDINGS, this._updateSessionsHandler);
44✔
660
    session.on(eventsEnum.DISCONNECTED, this._onSessionDisconnected);
44✔
661
    session.on(
44✔
662
      eventsEnum.WEBPHONE_SESSION_CONNECTED,
663
      this._updateSessionsHandler,
664
    );
665
    // Handle the session update at the end of function to reduce the probability of empty rc call
666
    // sessions
667
    this._updateSessionsHandler();
44✔
668
  }
669

670
  @action
671
  removeActiveSession() {
672
    this.data.activeSessionId = null;
1✔
673
  }
674

675
  // count it as load (should only call on container init step)
676
  @action
677
  setActiveSessionId(telephonySessionId: string) {
678
    if (!telephonySessionId) return;
1!
679
    this.data.activeSessionId = telephonySessionId;
1✔
680
  }
681

682
  @action
683
  setLastEndedSessionIds(session: WebphoneSession) {
684
    /**
685
     * don't add incoming call that isn't relied by current app
686
     *   to end sessions. this call can be answered by other apps
687
     */
688
    const normalizedWebphoneSession = normalizeWebphoneSession(session);
×
689
    if (
×
690
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
691
      !normalizedWebphoneSession.startTime &&
×
692
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
693
      !normalizedWebphoneSession.isToVoicemail &&
694
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
695
      !normalizedWebphoneSession.isForwarded &&
696
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
697
      !normalizedWebphoneSession.isReplied
698
    ) {
699
      return;
×
700
    }
701
    // @ts-expect-error TS(2339): Property 'partyData' does not exist on type 'Norma... Remove this comment to see the full error message
702
    const { partyData } = normalizedWebphoneSession;
×
703
    if (!partyData) return;
×
704
    if (this.lastEndedSessionIds.indexOf(partyData.sessionId) === -1) {
×
705
      this.lastEndedSessionIds = [partyData.sessionId]
×
706
        .concat(this.lastEndedSessionIds)
707
        .slice(0, 5);
708
    }
709
  }
710

711
  private _checkConnectivity() {
712
    if (
25,678✔
713
      this._deps.connectivityMonitor &&
77,034✔
714
      this._deps.connectivityMonitor.ready &&
715
      this._connectivity !== this._deps.connectivityMonitor.connectivity
716
    ) {
717
      this._connectivity = this._deps.connectivityMonitor.connectivity;
379✔
718

719
      if (this._connectivity) {
379✔
720
        this.fetchData();
359✔
721
      }
722
    }
723
  }
724

725
  private _getTrackEventName(name: string) {
726
    // TODO: refactor to remove `this.parentModule`.
727
    const currentPath = this._deps.routerInteraction?.currentPath;
×
728
    const showCallLog = (this.parentModule as any).callLogSection?.show;
×
729
    const showNotification = (this.parentModule as any).callLogSection
×
730
      ?.showNotification;
731
    if (showNotification) {
×
732
      return `${name}/Call notification page`;
×
733
    }
734
    if (showCallLog) {
×
735
      return `${name}/Call log page`;
×
736
    }
737
    if (currentPath === '/calls') {
×
738
      return `${name}/All calls page`;
×
739
    }
740
    if (currentPath.includes('/simplifycallctrl')) {
×
741
      return `${name}/Small call control`;
×
742
    }
743
    return name;
×
744
  }
745

746
  @action
747
  setCallControlBusyTimestamp() {
748
    this.data.busyTimestamp = Date.now();
1✔
749
  }
750

751
  @action
752
  clearCallControlBusyTimestamp() {
753
    this.data.busyTimestamp = 0;
1✔
754
  }
755

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

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

812
  async transferUnmuteHandler(telephonySessionId: string) {
813
    try {
×
814
      const session = this._getSessionById(telephonySessionId);
×
815
      if (session?.telephonySession?.party?.muted) {
×
816
        await session.unmute();
×
817
      }
818
    } catch (error: any /** TODO: confirm with instanceof */) {
819
      // https://jira_domain/browse/NTP-1308
820
      // Unmute before transfer due to we can not sync the mute status after transfer.
821
    }
822
  }
823

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

854
  getRecordingId(session: Session) {
855
    const recording = session.recordings[0];
×
856
    const recodingId = recording && recording.id;
×
857
    return recodingId;
×
858
  }
859

860
  @track((that: ActiveCallControl) => [
×
861
    that._getTrackEventName(trackEvents.stopRecord),
862
  ])
863
  @proxify
864
  async stopRecord(telephonySessionId: string) {
865
    try {
×
866
      this.setCallControlBusyTimestamp();
×
867
      const session = this._getSessionById(telephonySessionId);
×
868
      const recordingId = this.getRecordingId(session);
×
869
      await session.stopRecord({ recordingId });
×
870
      this.clearCallControlBusyTimestamp();
×
871
    } catch (error: any /** TODO: confirm with instanceof */) {
872
      console.log('stop record error:', error);
×
873

874
      this._deps.alert.danger({
×
875
        message: webphoneErrors.pauseRecordError,
876
      });
877

878
      this.clearCallControlBusyTimestamp();
×
879
    }
880
  }
881

882
  @track((that: ActiveCallControl) => [
×
883
    that._getTrackEventName(trackEvents.hangup),
884
  ])
885
  @proxify
886
  async hangUp(telephonySessionId: string) {
887
    try {
×
888
      this.setCallControlBusyTimestamp();
×
889
      const session = this._getSessionById(telephonySessionId);
×
890
      await session.hangup();
×
891

892
      this._onCallEndFunc?.();
×
893
      // TODO: find way to fix that 800ms
894
      // avoid hung up sync slow and user click multiple times.
895
      await sleep(800);
×
896
      this.clearCallControlBusyTimestamp();
×
897
    } catch (error: any) {
898
      // TODO: fix error handling with instanceof
899
      console.error('hangup error', error);
×
900
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
901
        this._deps.alert.warning({ message: callControlError.generalError });
×
902
      }
903
      this.clearCallControlBusyTimestamp();
×
904
    }
905
  }
906

907
  @track((that: ActiveCallControl) => [
×
908
    that._getTrackEventName(trackEvents.voicemail),
909
  ])
910
  @proxify
911
  async reject(telephonySessionId: string) {
912
    try {
×
913
      this.setCallControlBusyTimestamp();
×
914
      const session = this._getSessionById(telephonySessionId);
×
915

916
      // !If is a queue call, ignore is performed
917
      if (session.party.queueCall) {
×
918
        return await this.ignore(telephonySessionId);
×
919
      }
920

921
      await session.toVoicemail();
×
922

923
      if (session && session.webphoneSession) {
×
924
        (session.webphoneSession as WebphoneSession).__rc_isToVoicemail = true;
×
925
      }
926
      this.clearCallControlBusyTimestamp();
×
927
    } catch (error: any) {
928
      // TODO: fix error handling with instanceof
929
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
930
        this._deps.alert.warning({ message: callControlError.generalError });
×
931
      }
932
      this.clearCallControlBusyTimestamp();
×
933
    }
934
  }
935

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

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

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

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

1087
  @proxify
1088
  async toVoicemail(voicemailId: string, telephonySessionId: string) {
1089
    try {
×
1090
      this.setCallControlBusyTimestamp();
×
1091
      const session = this._getSessionById(telephonySessionId);
×
1092
      if (!session) {
×
1093
        return false;
×
1094
      }
1095
      await session.transfer(voicemailId, { type: 'voicemail' });
×
1096
      this.clearCallControlBusyTimestamp();
×
1097
      this._deps.alert.success({ message: callControlError.transferCompleted });
×
1098
    } catch (error: any /** TODO: confirm with instanceof */) {
1099
      console.error('toVoicemail error', error);
×
1100
      this._deps.alert.warning({ message: webphoneErrors.toVoiceMailError });
×
1101
      this.clearCallControlBusyTimestamp();
×
1102
    }
1103
  }
1104

1105
  @proxify
1106
  async completeWarmTransfer(telephonySession: string) {
1107
    try {
×
1108
      this.setCallControlBusyTimestamp();
×
1109

1110
      const { isOriginal, relatedTelephonySessionId } =
1111
        this.transferCallMapping[telephonySession];
×
1112

1113
      const session = this._getSessionById(
×
1114
        isOriginal ? telephonySession : relatedTelephonySessionId,
×
1115
      );
1116
      const transferSession = this._getSessionById(
×
1117
        isOriginal ? relatedTelephonySessionId : telephonySession,
×
1118
      );
1119

1120
      if (!transferSession) {
×
1121
        return false;
×
1122
      }
1123
      await session.warmTransfer(transferSession);
×
1124
      this.clearCallControlBusyTimestamp();
×
1125
      this._deps.alert.success({ message: callControlError.transferCompleted });
×
1126
    } catch (error: any /** TODO: confirm with instanceof */) {
1127
      console.error('warmTransfer error', error);
×
1128
      this._deps.alert.warning({ message: callControlError.generalError });
×
1129
      this.clearCallControlBusyTimestamp();
×
1130
    }
1131
  }
1132

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

1156
  async getValidPhoneNumber(phoneNumber: string, withMainNumber?: boolean) {
1157
    let validatedResult;
1158
    let validPhoneNumber;
1159
    if (!this._permissionCheck) {
3✔
1160
      validatedResult = validateNumbers({
1✔
1161
        allowRegionSettings: !!this._deps.brand.brandConfig.allowRegionSettings,
1162
        areaCode: this._deps.regionSettings.areaCode,
1163
        countryCode: this._deps.regionSettings.countryCode,
1164
        phoneNumbers: [phoneNumber],
1165
      });
1166
      validPhoneNumber = validatedResult[0];
1✔
1167
    } else {
1168
      const isEDPEnabled = this._deps.appFeatures?.isEDPEnabled;
2✔
1169
      validatedResult = isEDPEnabled
2!
1170
        ? this._deps.numberValidate.validate([phoneNumber])
1171
        : await this._deps.numberValidate.validateNumbers([phoneNumber]);
1172

1173
      if (!validatedResult.result) {
2✔
1174
        validatedResult.errors.forEach(async (error) => {
1✔
1175
          const isHAError =
1176
            // @ts-expect-error TS(2345): Argument of type '{ phoneNumber: string; type: "sp... Remove this comment to see the full error message
1177
            !!(await this._deps.availabilityMonitor?.checkIfHAError(error));
1✔
1178
          if (!isHAError) {
1!
1179
            // TODO: fix `callErrors` type
1180
            this._deps.alert.warning({
1✔
1181
              message: (callErrors as any)[error.type],
1182
              payload: {
1183
                phoneNumber: error.phoneNumber,
1184
              },
1185
            });
1186
          }
1187
        });
1188
        return;
1✔
1189
      }
1190
      if (isEDPEnabled) {
1!
1191
        const parsedNumbers = await this._deps.numberValidate.parseNumbers([
×
1192
          phoneNumber,
1193
        ]);
1194
        validPhoneNumber =
×
1195
          parsedNumbers?.[0].availableExtension ??
×
1196
          parsedNumbers?.[0].parsedNumber;
1197
      } else {
1198
        // TODO: fix `validatedResult` type in `numberValidate` module.
1199
        validPhoneNumber = (validatedResult as any).numbers?.[0]?.e164;
1✔
1200
      }
1201
    }
1202

1203
    let result = validPhoneNumber;
2✔
1204
    if (withMainNumber && validPhoneNumber.indexOf('+') === -1) {
2!
1205
      result = [
×
1206
        this._deps.accountInfo.mainCompanyNumber,
1207
        validPhoneNumber,
1208
      ].join('*');
1209
    }
1210

1211
    return result;
2✔
1212
  }
1213

1214
  // FIXME: Incomplete Implementation?
1215
  @proxify
1216
  async flip(flipValue: string, telephonySessionId: string) {
1217
    try {
×
1218
      this.setCallControlBusyTimestamp();
×
1219
      const session = this._getSessionById(telephonySessionId);
×
1220
      await session.flip({ callFlipId: flipValue });
×
1221
      this.clearCallControlBusyTimestamp();
×
1222
    } catch (error: any /** TODO: confirm with instanceof */) {
1223
      console.error('flip error', error);
×
1224
      this.clearCallControlBusyTimestamp();
×
1225
      throw error;
×
1226
    }
1227
  }
1228

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

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

1284
      await session.forward(validPhoneNumber, this.acceptOptions);
1✔
1285
      this._deps.alert.success({
×
1286
        message: callControlError.forwardSuccess,
1287
      });
1288
      if (typeof this._onCallEndFunc === 'function') {
×
1289
        this._onCallEndFunc();
×
1290
      }
1291
      return true;
×
1292
    } catch (e: any /** TODO: confirm with instanceof */) {
1293
      console.error(e);
1✔
1294
      this._deps.alert.warning({
1✔
1295
        message: webphoneErrors.unknownError,
1296
      });
1297
      return false;
1✔
1298
    }
1299
  }
1300

1301
  // DTMF handing by webphone session temporary, due to rc call session doesn't support currently
1302
  @proxify
1303
  async sendDTMF(dtmfValue: string, telephonySessionId: string) {
1304
    try {
×
1305
      const session = this._getSessionById(telephonySessionId);
×
1306
      // TODO: using rc call session
1307
      const { webphoneSession } = session;
×
1308
      if (webphoneSession) {
×
1309
        await webphoneSession.dtmf(dtmfValue, 100);
×
1310
      }
1311
    } catch (error: any /** TODO: confirm with instanceof */) {
1312
      console.log('send dtmf error', error);
×
1313
      throw error;
×
1314
    }
1315
  }
1316

1317
  private _onWebphoneInvite(session: WebphoneSession) {
1318
    const webphoneSession = session;
23✔
1319
    if (!webphoneSession) return;
23!
1320
    if (!webphoneSession.__rc_creationTime) {
23!
1321
      webphoneSession.__rc_creationTime = Date.now();
×
1322
    }
1323
    if (!webphoneSession.__rc_lastActiveTime) {
23!
1324
      webphoneSession.__rc_lastActiveTime = Date.now();
×
1325
    }
1326
    webphoneSession.on('terminated', () => {
23✔
1327
      console.log('Call Event: terminated');
2✔
1328
      // this.setLastEndedSessionIds(webphoneSession);
1329
      const { telephonySessionId } =
1330
        this.rcCallSessions.find(
2!
1331
          (s) => s.webphoneSession === webphoneSession,
4✔
1332
        ) || {};
1333

1334
      if (!telephonySessionId) return;
2!
1335

1336
      this._setActiveSessionIdFromOnHoldCalls(telephonySessionId);
×
1337
      this._onCallEnd(telephonySessionId);
×
1338
    });
1339
    webphoneSession.on('accepted', () => {
23✔
1340
      const { telephonySessionId } =
1341
        this.rcCallSessions.find(
17!
1342
          (s) => s.webphoneSession === webphoneSession,
41✔
1343
        ) || {};
1344

1345
      if (!telephonySessionId) return;
17!
1346

1347
      if (this._autoMergeWebphoneSessionsMap.get(webphoneSession)) {
×
1348
        this._autoMergeWebphoneSessionsMap.delete(webphoneSession);
×
1349
      } else {
1350
        this.setActiveSessionId(telephonySessionId);
×
1351
        this._holdOtherCalls(telephonySessionId);
×
1352
        this._addTrackToActiveSession();
×
1353
      }
1354
      this.updateActiveSessions();
×
1355
      this._onCallAccepted(telephonySessionId);
×
1356
    });
1357
  }
1358

1359
  @action
1360
  private _setRingSessionId(sessionId: string | null = null) {
×
1361
    this.data.ringSessionId = sessionId;
24✔
1362
  }
1363

1364
  /**
1365
   *if current call is terminated, then pick the first onhold call as active current call;
1366
   *
1367
   * @param {Session} session
1368
   * @memberof ActiveCallControl
1369
   */
1370
  private _setActiveSessionIdFromOnHoldCalls(telephonySessionId: string) {
1371
    if (!telephonySessionId) return;
×
1372
    if (this.activeSessionId === telephonySessionId) {
×
1373
      const onHoldSessions: ActiveCallControlSessionData[] = filter(
×
1374
        (s: ActiveCallControlSessionData) => isHolding(s),
×
1375
        this.sessions,
1376
      );
1377
      if (onHoldSessions.length) {
×
1378
        this.setActiveSessionId(onHoldSessions[0].telephonySessionId);
×
1379
      }
1380
    }
1381
  }
1382

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

1421
  @action
1422
  setPickUpCallData(data: IPickUpCallDataMap) {
1423
    this.pickUpCallDataMap = { ...data };
×
1424
  }
1425

1426
  @proxify
1427
  private async _answer(telephonySessionId: string) {
1428
    try {
×
1429
      this._triggerAutoMergeEvent(telephonySessionId);
×
1430
      this.setCallControlBusyTimestamp();
×
1431
      const session = this._getSessionById(telephonySessionId);
×
1432

1433
      this._activeSession = session;
×
1434
      await this._holdOtherCalls(telephonySessionId);
×
1435
      const { webphoneSession } = session;
×
1436
      const deviceId = this._deps.webphone?.device?.id;
×
1437
      if (webphoneSession) {
×
NEW
1438
        this._deps.webphone?.initWebphoneSessionEvents(webphoneSession);
×
UNCOV
1439
        await session.answer({ deviceId });
×
1440
      } else {
1441
        await this.pickUpCall({
×
1442
          ...this.pickUpCallDataMap[telephonySessionId],
1443
        });
1444
      }
1445
      this._trackWebRTCCallAnswer();
×
1446
      if (webphoneSession && webphoneSession.__rc_callStatus) {
×
1447
        webphoneSession.__rc_callStatus = sessionStatus.connected;
×
1448
      }
1449
    } finally {
1450
      this.clearCallControlBusyTimestamp();
×
1451
    }
1452
  }
1453

1454
  public async pickUpCall(data: IPickUpCallParams) {
1455
    const { telephonySessionId } = data;
×
1456
    await this._rcCall?.pickupInboundCall({
×
1457
      ...this.pickUpCallDataMap[telephonySessionId],
1458
      ...data,
1459
      ...this.acceptOptions,
1460
    });
1461
  }
1462

1463
  @track((that: ActiveCallControl) => [
×
1464
    that._getTrackEventName(trackEvents.answer),
1465
  ])
1466
  @proxify
1467
  async answer(telephonySessionId: string) {
1468
    try {
×
1469
      await this._answer(telephonySessionId);
×
1470
    } catch (error: any /** TODO: confirm with instanceof */) {
1471
      console.log('answer failed.');
×
1472
    }
1473
  }
1474

1475
  @track((that: ActiveCallControl) => [
×
1476
    that._getTrackEventName(trackEvents.holdAndAnswer),
1477
  ])
1478
  @proxify
1479
  async answerAndHold(telephonySessionId: string) {
1480
    // currently, the logic is same as answer
1481
    try {
×
1482
      await this._answer(telephonySessionId);
×
1483
    } catch (error: any /** TODO: confirm with instanceof */) {
1484
      console.log('answer hold failed.', error);
×
1485
    }
1486
  }
1487

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

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

1554
  async startWarmTransfer(transferNumber: string, telephonySessionId: string) {
1555
    // todo handle error;
1556
    const toNumber = await this.getValidPhoneNumber(transferNumber);
×
1557
    return this.makeCall({
×
1558
      toNumber,
1559
      transferSessionId: telephonySessionId,
1560
    });
1561
  }
1562

1563
  @action
1564
  setWarmTransferMapping(originalId: string, transferredId: string) {
1565
    this.transferCallMapping = {
×
1566
      ...this.transferCallMapping,
1567
      [originalId]: {
1568
        relatedTelephonySessionId: transferredId,
1569
        isOriginal: true,
1570
      },
1571
      [transferredId]: {
1572
        relatedTelephonySessionId: originalId,
1573
        isOriginal: false,
1574
      },
1575
    };
1576
  }
1577

1578
  @action
1579
  cleanCurrentWarmTransferData() {
1580
    const warmTransferSessionIds = Object.keys(this.transferCallMapping);
2✔
1581
    const currentSessionIds = this.sessions.map(
2✔
1582
      (session) => session.telephonySessionId,
4✔
1583
    );
1584
    const needRemovedIds = warmTransferSessionIds.filter(
2✔
1585
      (telephonySessionId) => !currentSessionIds.includes(telephonySessionId),
×
1586
    );
1587

1588
    if (needRemovedIds.length > 0) {
2!
1589
      const removeSessionSet = new Set(needRemovedIds);
×
1590

1591
      const filteredData = Object.fromEntries(
×
1592
        Object.entries(this.transferCallMapping).filter(
1593
          ([id, transferInfo]) =>
1594
            !(
×
1595
              removeSessionSet.has(id) ||
×
1596
              removeSessionSet.has(transferInfo.relatedTelephonySessionId)
1597
            ),
1598
        ),
1599
      );
1600

1601
      this.transferCallMapping = filteredData;
×
1602
    }
1603
  }
1604

1605
  @proxify
1606
  async makeCall(params: ModuleMakeCallParams) {
1607
    try {
×
1608
      if (
×
1609
        params.toNumber.length > 6 &&
×
1610
        (!this._deps.availabilityMonitor ||
1611
          !this._deps.availabilityMonitor.isVoIPOnlyMode)
1612
      ) {
1613
        const phoneLines = await this._fetchDL();
×
1614
        if (phoneLines.length === 0) {
×
1615
          this._deps.alert.warning({
×
1616
            message: webphoneErrors.noOutboundCallWithoutDL,
1617
          });
1618
          return null;
×
1619
        }
1620
      }
1621
      await this._holdOtherCalls();
×
1622
      const sdkMakeCallParams: MakeCallParams = {
×
1623
        // type 'callControl' not support webphone's sip device currently.
1624
        type: 'webphone',
1625
        toNumber: params.toNumber,
1626
        fromNumber: params.fromNumber,
1627
        homeCountryId: params.homeCountryId,
1628
      };
1629
      const session = (await this._rcCall!.makeCall(
×
1630
        sdkMakeCallParams,
1631
      )) as Session;
1632
      this._activeSession = session;
×
NEW
1633
      this._deps.webphone?.initWebphoneSessionEvents(session.webphoneSession);
×
NEW
1634
      session.webphoneSession.on('progress', (incomingResponse) => {
×
UNCOV
1635
        if (
×
1636
          session.telephonySessionId &&
×
1637
          this.activeSessionId !== session.telephonySessionId
1638
        ) {
1639
          this.setActiveSessionId(session.telephonySessionId);
×
1640

1641
          const { transferSessionId } = params;
×
1642
          if (transferSessionId) {
×
1643
            this.setWarmTransferMapping(
×
1644
              transferSessionId,
1645
              session.telephonySessionId,
1646
            );
1647
          }
1648
        }
1649
      });
1650
      this._triggerAutoMergeEvent();
×
1651
      return session;
×
1652
    } catch (error: any /** TODO: confirm with instanceof */) {
1653
      console.log('make call fail.', error);
×
1654
    }
1655
  }
1656

1657
  private async _fetchDL() {
1658
    const response = await this._deps.client
×
1659
      .account()
1660
      .extension()
1661
      .device()
1662
      .list();
1663
    const devices = response.records;
×
1664
    let phoneLines: any[] = [];
×
1665
    devices?.forEach((device) => {
×
1666
      // wrong type of phoneLines, temporary treat it as any
1667
      if (!device.phoneLines || (device.phoneLines as any).length === 0) {
×
1668
        return;
×
1669
      }
1670
      phoneLines = phoneLines.concat(device.phoneLines);
×
1671
    });
1672
    return phoneLines;
×
1673
  }
1674

1675
  getActiveSession(telephonySessionId: string | null) {
1676
    if (!telephonySessionId) {
7!
1677
      return null;
7✔
1678
    }
1679
    return this.activeSessions[telephonySessionId];
×
1680
  }
1681

1682
  getSession(telephonySessionId: string) {
1683
    return this.sessions.find(
×
1684
      (session) => session.telephonySessionId === telephonySessionId,
×
1685
    );
1686
  }
1687

1688
  @computed(({ activeSessionId, activeSessions }: ActiveCallControl) => [
65✔
1689
    activeSessionId,
1690
    activeSessions,
1691
  ])
1692
  get activeSession() {
1693
    return this.getActiveSession(this.activeSessionId);
7✔
1694
  }
1695

1696
  @computed(({ ringSessionId, activeSessions }: ActiveCallControl) => [
×
1697
    ringSessionId,
1698
    activeSessions,
1699
  ])
1700
  get ringSession() {
1701
    return this.getActiveSession(this.ringSessionId);
×
1702
  }
1703

1704
  @computed(({ sessions }: ActiveCallControl) => [sessions])
×
1705
  get ringSessions() {
1706
    if (!this.sessions) {
×
1707
      return [];
×
1708
    }
1709
    return this.sessions.filter((session: ActiveCallControlSessionData) =>
×
1710
      isRinging(session),
×
1711
    );
1712
  }
1713

1714
  @computed((that: ActiveCallControl) => [that.sessions, that.timestamp])
65✔
1715
  get activeSessions() {
1716
    return this.sessions.reduce((accumulator, session) => {
7✔
1717
      const { id } = session;
×
1718
      accumulator[id!] = normalizeSession({ session });
×
1719
      return accumulator;
×
1720
    }, {} as Record<string, Partial<ActiveSession>>);
1721
  }
1722

1723
  @computed((that: ActiveCallControl) => [that._deps.presence.calls])
×
1724
  get sessionIdToTelephonySessionIdMapping() {
1725
    return this._deps.presence.calls.reduce((accumulator, call) => {
×
1726
      const { telephonySessionId, sessionId } = call;
×
1727
      accumulator[sessionId!] = telephonySessionId!;
×
1728
      return accumulator;
×
1729
    }, {} as Record<string, string>);
1730
  }
1731

1732
  /**
1733
   * Mitigation strategy for avoiding 404/409 on call control endpoints.
1734
   * This should gradually move towards per session controls rather than
1735
   * a global busy timeout.
1736
   */
1737
  get busy() {
1738
    return Date.now() - this.busyTimestamp < DEFAULT_BUSY_TIMEOUT;
266✔
1739
  }
1740

1741
  // This should reflect on the app permissions setting in DWP
1742
  get hasPermission() {
1743
    return this._deps.appFeatures.hasCallControl;
26,358✔
1744
  }
1745

1746
  get timeToRetry() {
1747
    return this._timeToRetry;
×
1748
  }
1749

1750
  get ttl() {
1751
    return this._ttl;
×
1752
  }
1753

1754
  get acceptOptions() {
1755
    return {
×
1756
      sessionDescriptionHandlerOptions: {
1757
        constraints: {
1758
          audio: {
1759
            deviceId: this._deps.audioSettings?.inputDeviceId,
1760
          },
1761
          video: false,
1762
        },
1763
      },
1764
    };
1765
  }
1766

1767
  get hasCallInRecording() {
1768
    return this.sessions.some((session) => isRecording(session));
×
1769
  }
1770

1771
  // TODO:refactor, use this.sessions instead
1772
  get rcCallSessions() {
1773
    return filter(
19✔
1774
      (session) => filterDisconnectedCalls(session),
47✔
1775
      this._rcCall?.sessions || [],
19!
1776
    );
1777
  }
1778

1779
  get activeSessionId() {
1780
    return this.data.activeSessionId;
100,031✔
1781
  }
1782

1783
  get busyTimestamp() {
1784
    return this.data.busyTimestamp;
266✔
1785
  }
1786

1787
  get timestamp() {
1788
    return this.data.timestamp;
65✔
1789
  }
1790

1791
  get sessions() {
1792
    return this.data.sessions;
98✔
1793
  }
1794

1795
  get ringSessionId() {
1796
    return this.data.ringSessionId;
×
1797
  }
1798

1799
  @track(trackEvents.inboundWebRTCCallConnected)
1800
  _trackWebRTCCallAnswer() {
1801
    //
1802
  }
1803

1804
  @track(trackEvents.dialpadOpen)
1805
  dialpadOpenTrack() {
1806
    //
1807
  }
1808

1809
  @track(trackEvents.dialpadClose)
1810
  dialpadCloseTrack() {
1811
    //
1812
  }
1813

1814
  @track((that: ActiveCallControl) => [
×
1815
    that._getTrackEventName(trackEvents.clickTransfer),
1816
  ])
1817
  clickTransferTrack() {
1818
    //
1819
  }
1820

1821
  @track((that: ActiveCallControl) => [
×
1822
    that._getTrackEventName(trackEvents.forward),
1823
  ])
1824
  clickForwardTrack() {
1825
    //
1826
  }
1827

1828
  @track((that: ActiveCallControl, path: string) => {
1829
    return (analytics) => {
×
1830
      // @ts-expect-error TS(2339): Property 'getTrackTarget' does not exist on type '... Remove this comment to see the full error message
1831
      const target = analytics.getTrackTarget();
×
1832
      return [
×
1833
        trackEvents.openEntityDetailLink,
1834
        { path: path || target.router },
×
1835
      ];
1836
    };
1837
  })
1838
  openEntityDetailLinkTrack(path: string) {
1839
    //
1840
  }
1841

1842
  @track((that: ActiveCallControl) => [
×
1843
    that._getTrackEventName(trackEvents.switch),
1844
  ])
1845
  clickSwitchTrack() {
1846
    //
1847
  }
1848

1849
  private _getSessionById(sessionId: string) {
1850
    const session = this._rcCall!.sessions.find((s) => s.id === sessionId);
4✔
1851

1852
    return session as Session;
4✔
1853
  }
1854
}
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