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

ringcentral / ringcentral-js-widgets / 4878562030

pending completion
4878562030

push

github

GitHub
sync features and bugfixs from e3d1ac7 (#1714)

1724 of 6102 branches covered (28.25%)

Branch coverage included in aggregate %.

751 of 2684 new or added lines in 97 files covered. (27.98%)

457 existing lines in 34 files now uncovered.

3248 of 8911 relevant lines covered (36.45%)

18.43 hits per line

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

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

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

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

68
@Module({
69
  name: 'ActiveCallControl',
70
  deps: [
71
    'Auth',
72
    'Alert',
73
    'Brand',
74
    'Client',
75
    'Presence',
76
    'AccountInfo',
77
    'Subscription',
78
    'ExtensionInfo',
79
    'NumberValidate',
80
    'RegionSettings',
81
    'ConnectivityMonitor',
82
    'AppFeatures',
83
    { dep: 'Prefix', optional: true },
84
    { dep: 'Storage', optional: true },
85
    { dep: 'Webphone', optional: true },
86
    { dep: 'TabManager', optional: true },
87
    { dep: 'AudioSettings', optional: true },
88
    { dep: 'AvailabilityMonitor', optional: true },
89
    { dep: 'ActiveCallControlOptions', optional: true },
90
    { dep: 'RouterInteraction', optional: true },
91
  ],
92
})
93
export class ActiveCallControl extends RcModuleV2<Deps> {
94
  private _onCallEndFunc?: () => void;
95
  private _onCallSwitchedFunc?: (sessionId: string) => void;
96

97
  private _connectivity = false;
3✔
98
  private _timeoutId: ReturnType<typeof setTimeout> | null = null;
3✔
99
  private _lastSubscriptionMessage: MessageBase | null = null;
3✔
100
  private _activeSession?: Session;
101

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

116
  constructor(deps: Deps) {
117
    super({
3✔
118
      deps,
119
      enableCache: deps.activeCallControlOptions?.enableCache ?? true,
6✔
120
      storageKey: 'activeCallControl',
121
    });
122
  }
123

124
  override async onStateChange() {
NEW
125
    if (this.ready && this.hasPermission) {
×
NEW
126
      this._subscriptionHandler();
×
NEW
127
      this._checkConnectivity();
×
128
    }
129
  }
130

131
  override async _initModule() {
NEW
132
    this._createOtherInstanceListener();
×
NEW
133
    await super._initModule();
×
134
  }
135

136
  _createOtherInstanceListener() {
NEW
137
    if (!this._deps.tabManager || !this._enableAutoSwitchFeature) {
×
NEW
138
      return;
×
139
    }
NEW
140
    window.addEventListener('storage', (e) => {
×
NEW
141
      this._onStorageChangeEvent(e);
×
142
    });
143
  }
144

145
  _onStorageChangeEvent(e: StorageEvent) {
NEW
146
    switch (e.key) {
×
147
      case this._autoMergeSignCallIdKey:
NEW
148
        this._triggerCurrentClientAutoMerge(e);
×
NEW
149
        break;
×
150
      case this._autoMergeCallsKey:
NEW
151
        this._autoMergeCallsHandler(e);
×
NEW
152
        break;
×
153
      default:
NEW
154
        break;
×
155
    }
156
  }
157

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

181
  async _autoMergeCallsHandler(e: StorageEvent) {
NEW
182
    if (!this._deps.tabManager?.active) return;
×
183

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

NEW
207
          if (!activeCall) {
×
NEW
208
            console.log(
×
209
              `Auto Switch failed with telephonySessionId ${telephonySessionId}`,
210
            );
NEW
211
            return acc;
×
212
          }
213

NEW
214
          acc.push(activeCall);
×
NEW
215
          return acc;
×
216
        }, [] as ActiveCallInfo[]);
217

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

247
  _triggerAutoMergeEvent(telephoneSessionId?: string) {
NEW
248
    if (!this._deps.tabManager || !this._enableAutoSwitchFeature) return;
×
249

NEW
250
    const id = uuidV4();
×
NEW
251
    const data = {
×
252
      id,
253
      telephoneSessionId,
254
    };
NEW
255
    localStorage.setItem(this._autoMergeSignCallIdKey, JSON.stringify(data));
×
256
  }
257

258
  _addTrackToActiveSession() {
NEW
259
    const telephonySessionId = this.activeSessionId;
×
260
    const activeRCCallSession =
NEW
261
      this.rcCallSessions.find(
×
NEW
262
        (s) => s.telephonySessionId === telephonySessionId,
×
263
      ) || this._activeSession;
NEW
264
    if (
×
265
      activeRCCallSession &&
×
266
      activeRCCallSession.webphoneSession &&
267
      this._deps.webphone
268
    ) {
NEW
269
      const { _remoteVideo, _localVideo } = this._deps.webphone;
×
NEW
270
      activeRCCallSession.webphoneSession.addTrack(_remoteVideo, _localVideo);
×
271
    }
272
  }
273

274
  @storage
275
  @state
276
  transferCallMapping: ITransferCallSessionMapping = {};
3✔
277

278
  @storage
279
  @state
280
  data: {
281
    activeSessionId: string | null;
282
    busyTimestamp: number;
283
    timestamp: number;
284
    sessions: ActiveCallControlSessionData[];
285
    ringSessionId: string | null;
286
  } = {
3✔
287
    activeSessionId: null,
288
    busyTimestamp: 0,
289
    timestamp: 0,
290
    sessions: [],
291
    ringSessionId: null,
292
  };
293

294
  @state
295
  currentDeviceCallsMap: ICurrentDeviceCallsMap = {};
3✔
296

297
  @state
298
  lastEndedSessionIds: string[] = [];
3✔
299

300
  // TODO: conference call using
301
  @state
302
  cachedSessions: object[] = [];
3✔
303

304
  override async onInit() {
NEW
305
    if (!this.hasPermission) return;
×
306

NEW
307
    this._deps.subscription.subscribe([subscribeEvent]);
×
NEW
308
    this._rcCall = this._initRcCall();
×
309

NEW
310
    if (this._shouldFetch()) {
×
NEW
311
      try {
×
NEW
312
        await this.fetchData();
×
313
      } catch (e: any /** TODO: confirm with instanceof */) {
NEW
314
        this._retry();
×
315
      }
NEW
316
    } else if (this._polling) {
×
NEW
317
      this._startPolling();
×
318
    } else {
NEW
319
      this._retry();
×
320
    }
321
  }
322

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

NEW
356
    return rcCall;
×
357
  }
358

359
  override onInitOnce() {
NEW
360
    if (this._deps.webphone) {
×
NEW
361
      watch(
×
362
        this,
NEW
363
        () => this._deps.webphone?.connected,
×
364
        (newValue) => {
NEW
365
          if (newValue && this._deps.webphone?._webphone) {
×
NEW
366
            this._rcCall?.setWebphone(this._deps.webphone._webphone);
×
367
          }
368
        },
369
      );
370

NEW
371
      watch(
×
372
        this,
NEW
373
        () => this.activeSessionId,
×
374
        () => {
NEW
375
          this._addTrackToActiveSession();
×
376
        },
377
      );
378
    }
379
  }
380

381
  override onReset() {
NEW
382
    this.resetState();
×
383
  }
384

385
  @action
386
  resetState() {
NEW
387
    this.data.activeSessionId = null;
×
NEW
388
    this.data.busyTimestamp = 0;
×
NEW
389
    this.data.timestamp = 0;
×
NEW
390
    this.data.sessions = [];
×
391
  }
392

393
  _shouldFetch() {
NEW
394
    return !this._deps.tabManager || this._deps.tabManager.active;
×
395
  }
396

397
  @proxify
398
  async fetchData() {
NEW
399
    if (!this._promise) {
×
NEW
400
      this._promise = this._fetchData();
×
401
    }
NEW
402
    await this._promise;
×
403
  }
404

405
  _clearTimeout() {
NEW
406
    if (this._timeoutId) clearTimeout(this._timeoutId);
×
407
  }
408

409
  _subscriptionHandler() {
NEW
410
    let { message } = this._deps.subscription;
×
NEW
411
    if (
×
412
      message &&
×
413
      // FIXME: is that object compare is fine, should confirm that?
414
      message !== this._lastSubscriptionMessage &&
415
      message.event &&
416
      telephonySessionsEndPoint.test(message.event) &&
417
      message.body
418
    ) {
NEW
419
      message = this._checkRingOutCallDirection(message);
×
NEW
420
      this._lastSubscriptionMessage = message;
×
NEW
421
      if (this._rcCall) {
×
NEW
422
        this._rcCall.onNotificationEvent(message);
×
423
      }
424
    }
425
  }
426

427
  // TODO: workaround of PLA bug: https://jira.ringcentral.com/browse/PLA-52742, remove these code after PLA
428
  // fixed this bug
429
  private _checkRingOutCallDirection(message: ExtensionTelephonySessionsEvent) {
NEW
430
    const { body } = message;
×
NEW
431
    const originType = body?.origin?.type;
×
432

NEW
433
    if (body && originType === 'RingOut') {
×
NEW
434
      const { parties } = body;
×
NEW
435
      if (Array.isArray(parties) && parties.length) {
×
NEW
436
        forEach((party: any) => {
×
NEW
437
          if (
×
438
            party.ringOutRole &&
×
439
            party.ringOutRole === 'Initiator' &&
440
            party.direction === 'Inbound'
441
          ) {
NEW
442
            const tempFrom = { ...party.from };
×
NEW
443
            party.direction = 'Outbound';
×
NEW
444
            party.from = party.to;
×
NEW
445
            party.to = tempFrom;
×
446
          }
447
        }, parties);
448
      }
449
    }
NEW
450
    return message;
×
451
  }
452

453
  private _retry(t = this.timeToRetry) {
×
NEW
454
    this._clearTimeout();
×
NEW
455
    this._timeoutId = setTimeout(() => {
×
NEW
456
      this._timeoutId = null;
×
NEW
457
      if (!this.timestamp || Date.now() - this.timestamp > this.ttl) {
×
NEW
458
        if (!this._deps.tabManager || this._deps.tabManager.active) {
×
NEW
459
          this.fetchData();
×
460
        } else {
461
          // continue retry checks in case tab becomes main tab
NEW
462
          this._retry();
×
463
        }
464
      }
465
    }, t);
466
  }
467

468
  @proxify
469
  async _fetchData() {
NEW
470
    try {
×
NEW
471
      await this._syncData();
×
NEW
472
      if (this._polling) {
×
NEW
473
        this._startPolling();
×
474
      }
NEW
475
      this._promise = null;
×
476
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
477
      this._promise = null;
×
NEW
478
      if (this._polling) {
×
NEW
479
        this._startPolling(this.timeToRetry);
×
480
      } else {
NEW
481
        this._retry();
×
482
      }
NEW
483
      throw error;
×
484
    }
485
  }
486

487
  private _startPolling(t = this.timestamp + this.ttl + 10 - Date.now()) {
×
NEW
488
    this._clearTimeout();
×
NEW
489
    this._timeoutId = setTimeout(() => {
×
NEW
490
      this._timeoutId = null;
×
NEW
491
      if (!this._deps.tabManager || this._deps.tabManager?.active) {
×
NEW
492
        if (!this.timestamp || Date.now() - this.timestamp > this.ttl) {
×
NEW
493
          this.fetchData();
×
494
        } else {
NEW
495
          this._startPolling();
×
496
        }
NEW
497
      } else if (this.timestamp && Date.now() - this.timestamp < this.ttl) {
×
NEW
498
        this._startPolling();
×
499
      } else {
NEW
500
        this._startPolling(this.timeToRetry);
×
501
      }
502
    }, t);
503
  }
504

505
  async _syncData() {
NEW
506
    try {
×
NEW
507
      const activeCalls = this._deps.presence.calls;
×
NEW
508
      await this._rcCall!.loadSessions(activeCalls);
×
NEW
509
      this.updateActiveSessions();
×
NEW
510
      this._rcCall!.sessions.forEach((session) => {
×
NEW
511
        this._newSessionHandler(session as Session);
×
512
      });
513
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
514
      console.log('sync data error:', error);
×
NEW
515
      throw error;
×
516
    }
517
  }
518

519
  private _onSessionDisconnected = () => {
3✔
NEW
520
    this.updateActiveSessions();
×
NEW
521
    if (!this._deps.tabManager || this._deps.tabManager?.active) {
×
NEW
522
      this.cleanCurrentWarmTransferData();
×
523
    }
524
  };
525

526
  private _updateSessionsStatusHandler = ({
3✔
527
    status,
528
    telephonySessionId,
529
  }: {
530
    status: PartyStatusCode;
531
    telephonySessionId: string;
532
  }) => {
NEW
533
    this.updateActiveSessions();
×
534

NEW
535
    if (
×
536
      status === PartyStatusCode.answered &&
×
537
      this.activeSessionId !== telephonySessionId
538
    ) {
NEW
539
      this.setActiveSessionId(telephonySessionId);
×
540
    }
541
  };
542

543
  private _updateSessionsHandler = () => {
3✔
NEW
544
    this.updateActiveSessions();
×
545
  };
546

547
  @proxify
548
  async _onNewCall(session: Session) {
NEW
549
    this.updateActiveSessions();
×
NEW
550
    const ringSession = find(
×
NEW
551
      (x) => isRinging(x) && x.id === session.id,
×
552
      this.sessions,
553
    );
NEW
554
    const sessionId = ringSession?.id;
×
555

NEW
556
    this._setRingSessionId(sessionId);
×
557
  }
558

559
  @action
560
  private _onCallAccepted(telephonySessionId: string) {
NEW
561
    if (this.ringSessionId === telephonySessionId) {
×
NEW
562
      this.data.ringSessionId = this.ringSessions[0]?.id || null;
×
563
    }
564
  }
565

566
  @action
567
  private _onCallEnd(telephonySessionId: string) {
NEW
568
    if (this.ringSessionId === telephonySessionId) {
×
NEW
569
      this.data.ringSessionId = this.ringSessions[0]?.id || null;
×
570
    }
571
  }
572

573
  updateActiveSessions() {
574
    const currentDeviceCallsMap: ICurrentDeviceCallsMap = {};
1✔
575
    const callControlSessions = (this._rcCall?.sessions || [])
1!
576
      .filter((session) => filterDisconnectedCalls(session))
1✔
577
      .map((session) => {
578
        // @ts-expect-error
579
        currentDeviceCallsMap[session.telephonySessionId] =
1✔
580
          // @ts-expect-error
581
          normalizeWebphoneSession(session.webphoneSession);
582

583
        return {
1✔
584
          ...session.data,
585
          activeCallId: session.activeCallId,
586
          direction: session.direction,
587
          from: session.from,
588
          id: session.id,
589
          otherParties: session.otherParties,
590
          party: session.party || {},
1!
591
          recordings: session.recordings,
592
          isRecording: isOnRecording(session.recordings),
593
          sessionId: session.sessionId,
594
          startTime: session.startTime,
595
          status: session.status,
596
          telephonySessionId: session.telephonySessionId,
597
          telephonySession: normalizeTelephonySession(session.telephonySession),
598
          to: session.to,
599
        };
600
      });
601
    this._updateActiveSessions(currentDeviceCallsMap, callControlSessions);
1✔
602
  }
603

604
  @action
605
  private _updateActiveSessions(
606
    currentDeviceCallsMap: ICurrentDeviceCallsMap,
607
    callControlSessions: ActiveCallControlSessionData[],
608
  ) {
609
    this.data.timestamp = Date.now();
1✔
610
    this.currentDeviceCallsMap = currentDeviceCallsMap;
1✔
611
    this.data.sessions = callControlSessions || [];
1!
612
  }
613

614
  private _newSessionHandler(session: Session) {
NEW
615
    session.removeListener(
×
616
      eventsEnum.STATUS,
617
      this._updateSessionsStatusHandler,
618
    );
NEW
619
    session.removeListener(eventsEnum.MUTED, this._updateSessionsHandler);
×
NEW
620
    session.removeListener(eventsEnum.RECORDINGS, this._updateSessionsHandler);
×
NEW
621
    session.removeListener(
×
622
      eventsEnum.DISCONNECTED,
623
      this._onSessionDisconnected,
624
    );
NEW
625
    session.removeListener(
×
626
      eventsEnum.WEBPHONE_SESSION_CONNECTED,
627
      this._updateSessionsHandler,
628
    );
NEW
629
    session.on(eventsEnum.STATUS, this._updateSessionsStatusHandler);
×
NEW
630
    session.on(eventsEnum.MUTED, this._updateSessionsHandler);
×
NEW
631
    session.on(eventsEnum.RECORDINGS, this._updateSessionsHandler);
×
NEW
632
    session.on(eventsEnum.DISCONNECTED, this._onSessionDisconnected);
×
NEW
633
    session.on(
×
634
      eventsEnum.WEBPHONE_SESSION_CONNECTED,
635
      this._updateSessionsHandler,
636
    );
637
    // Handle the session update at the end of function to reduce the probability of empty rc call
638
    // sessions
NEW
639
    this._updateSessionsHandler();
×
640
  }
641

642
  @action
643
  removeActiveSession() {
644
    this.data.activeSessionId = null;
1✔
645
  }
646

647
  // count it as load (should only call on container init step)
648
  @action
649
  setActiveSessionId(telephonySessionId: string) {
650
    if (!telephonySessionId) return;
1!
651
    this.data.activeSessionId = telephonySessionId;
1✔
652
  }
653

654
  @action
655
  setLastEndedSessionIds(session: WebphoneSession) {
656
    /**
657
     * don't add incoming call that isn't relied by current app
658
     *   to end sessions. this call can be answered by other apps
659
     */
NEW
660
    const normalizedWebphoneSession = normalizeWebphoneSession(session);
×
NEW
661
    if (
×
662
      // @ts-expect-error
663
      !normalizedWebphoneSession.startTime &&
×
664
      // @ts-expect-error
665
      !normalizedWebphoneSession.isToVoicemail &&
666
      // @ts-expect-error
667
      !normalizedWebphoneSession.isForwarded &&
668
      // @ts-expect-error
669
      !normalizedWebphoneSession.isReplied
670
    ) {
NEW
671
      return;
×
672
    }
673
    // @ts-expect-error
NEW
674
    const { partyData } = normalizedWebphoneSession;
×
NEW
675
    if (!partyData) return;
×
NEW
676
    if (this.lastEndedSessionIds.indexOf(partyData.sessionId) === -1) {
×
NEW
677
      this.lastEndedSessionIds = [partyData.sessionId]
×
678
        .concat(this.lastEndedSessionIds)
679
        .slice(0, 5);
680
    }
681
  }
682

683
  private _checkConnectivity() {
NEW
684
    if (
×
685
      this._deps.connectivityMonitor &&
×
686
      this._deps.connectivityMonitor.ready &&
687
      this._connectivity !== this._deps.connectivityMonitor.connectivity
688
    ) {
NEW
689
      this._connectivity = this._deps.connectivityMonitor.connectivity;
×
690

NEW
691
      if (this._connectivity) {
×
NEW
692
        this.fetchData();
×
693
      }
694
    }
695
  }
696

697
  private _getTrackEventName(name: string) {
698
    // TODO: refactor to remove `this.parentModule`.
NEW
699
    const currentPath = this._deps.routerInteraction?.currentPath;
×
NEW
700
    const showCallLog = (this.parentModule as any).callLogSection?.show;
×
NEW
701
    const showNotification = (this.parentModule as any).callLogSection
×
702
      ?.showNotification;
NEW
703
    if (showNotification) {
×
NEW
704
      return `${name}/Call notification page`;
×
705
    }
NEW
706
    if (showCallLog) {
×
NEW
707
      return `${name}/Call log page`;
×
708
    }
NEW
709
    if (currentPath === '/calls') {
×
NEW
710
      return `${name}/All calls page`;
×
711
    }
NEW
712
    if (currentPath.includes('/simplifycallctrl')) {
×
NEW
713
      return `${name}/Small call control`;
×
714
    }
NEW
715
    return name;
×
716
  }
717

718
  @action
719
  setCallControlBusyTimestamp() {
720
    this.data.busyTimestamp = Date.now();
1✔
721
  }
722

723
  @action
724
  clearCallControlBusyTimestamp() {
725
    this.data.busyTimestamp = 0;
1✔
726
  }
727

NEW
728
  @track((that: ActiveCallControl) => [
×
729
    that._getTrackEventName(trackEvents.mute),
730
  ])
731
  @proxify
732
  async mute(telephonySessionId: string) {
NEW
733
    try {
×
NEW
734
      this.setCallControlBusyTimestamp();
×
NEW
735
      const session = this._getSessionById(telephonySessionId);
×
NEW
736
      await session.mute();
×
NEW
737
      this.clearCallControlBusyTimestamp();
×
738
    } catch (error: any) {
739
      // TODO: fix error handling with instanceof
NEW
740
      if (error.response && !error.response._text) {
×
NEW
741
        error.response._text = await error.response.clone().text();
×
742
      }
NEW
743
      if (conflictError(error)) {
×
NEW
744
        this._deps.alert.warning({
×
745
          message: callControlError.muteConflictError,
746
        });
NEW
747
      } else if (
×
748
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
749
      ) {
NEW
750
        this._deps.alert.warning({ message: callControlError.generalError });
×
751
      }
NEW
752
      this.clearCallControlBusyTimestamp();
×
753
    }
754
  }
755

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

784
  async transferUnmuteHandler(telephonySessionId: string) {
NEW
785
    try {
×
NEW
786
      const session = this._getSessionById(telephonySessionId);
×
NEW
787
      if (session?.telephonySession?.party?.muted) {
×
NEW
788
        await session.unmute();
×
789
      }
790
    } catch (error: any /** TODO: confirm with instanceof */) {
791
      // https://jira.ringcentral.com/browse/NTP-1308
792
      // Unmute before transfer due to we can not sync the mute status after transfer.
793
    }
794
  }
795

NEW
796
  @track((that: ActiveCallControl) => [
×
797
    that._getTrackEventName(trackEvents.record),
798
  ])
799
  @proxify
800
  async startRecord(telephonySessionId: string) {
NEW
801
    try {
×
NEW
802
      this.setCallControlBusyTimestamp();
×
NEW
803
      const session = this._getSessionById(telephonySessionId);
×
NEW
804
      const recordingId = this.getRecordingId(session);
×
NEW
805
      await session.startRecord({ recordingId });
×
NEW
806
      this.clearCallControlBusyTimestamp();
×
NEW
807
      return true;
×
808
    } catch (error: any) {
809
      // TODO: fix error handling with instanceof
NEW
810
      this.clearCallControlBusyTimestamp();
×
NEW
811
      const { errors = [] } = (await error.response.clone().json()) || {};
×
NEW
812
      if (errors.length) {
×
NEW
813
        for (const error of errors) {
×
NEW
814
          console.error('record fail:', error);
×
815
        }
NEW
816
        this._deps.alert.danger({
×
817
          message: webphoneErrors.recordError,
818
          payload: {
819
            errorCode: errors[0].errorCode,
820
          },
821
        });
822
      }
823
    }
824
  }
825

826
  getRecordingId(session: Session) {
NEW
827
    const recording = session.recordings[0];
×
NEW
828
    const recodingId = recording && recording.id;
×
NEW
829
    return recodingId;
×
830
  }
831

NEW
832
  @track((that: ActiveCallControl) => [
×
833
    that._getTrackEventName(trackEvents.stopRecord),
834
  ])
835
  @proxify
836
  async stopRecord(telephonySessionId: string) {
NEW
837
    try {
×
NEW
838
      this.setCallControlBusyTimestamp();
×
NEW
839
      const session = this._getSessionById(telephonySessionId);
×
NEW
840
      const recordingId = this.getRecordingId(session);
×
NEW
841
      await session.stopRecord({ recordingId });
×
NEW
842
      this.clearCallControlBusyTimestamp();
×
843
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
844
      console.log('stop record error:', error);
×
845

NEW
846
      this._deps.alert.danger({
×
847
        message: webphoneErrors.pauseRecordError,
848
      });
849

NEW
850
      this.clearCallControlBusyTimestamp();
×
851
    }
852
  }
853

NEW
854
  @track((that: ActiveCallControl) => [
×
855
    that._getTrackEventName(trackEvents.hangup),
856
  ])
857
  @proxify
858
  async hangUp(telephonySessionId: string) {
NEW
859
    try {
×
NEW
860
      this.setCallControlBusyTimestamp();
×
NEW
861
      const session = this._getSessionById(telephonySessionId);
×
NEW
862
      await session.hangup();
×
863

NEW
864
      this._onCallEndFunc?.();
×
865
      // TODO: find way to fix that 800ms
866
      // avoid hung up sync slow and user click multiple times.
NEW
867
      await sleep(800);
×
NEW
868
      this.clearCallControlBusyTimestamp();
×
869
    } catch (error: any) {
870
      // TODO: fix error handling with instanceof
NEW
871
      console.error('hangup error', error);
×
NEW
872
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
NEW
873
        this._deps.alert.warning({ message: callControlError.generalError });
×
874
      }
NEW
875
      this.clearCallControlBusyTimestamp();
×
876
    }
877
  }
878

NEW
879
  @track((that: ActiveCallControl) => [
×
880
    that._getTrackEventName(trackEvents.voicemail),
881
  ])
882
  @proxify
883
  async reject(telephonySessionId: string) {
NEW
884
    try {
×
NEW
885
      this.setCallControlBusyTimestamp();
×
NEW
886
      const session = this._getSessionById(telephonySessionId);
×
887

888
      // !If is a queue call, ignore is performed
NEW
889
      if (session.party.queueCall) {
×
NEW
890
        return await this.ignore(telephonySessionId);
×
891
      }
892

NEW
893
      await session.toVoicemail();
×
894

NEW
895
      if (session && session.webphoneSession) {
×
NEW
896
        (session.webphoneSession as WebphoneSession).__rc_isToVoicemail = true;
×
897
      }
NEW
898
      this.clearCallControlBusyTimestamp();
×
899
    } catch (error: any) {
900
      // TODO: fix error handling with instanceof
NEW
901
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
NEW
902
        this._deps.alert.warning({ message: callControlError.generalError });
×
903
      }
NEW
904
      this.clearCallControlBusyTimestamp();
×
905
    }
906
  }
907

NEW
908
  @track((that: ActiveCallControl) => [
×
909
    that._getTrackEventName(trackEvents.confirmSwitch),
910
  ])
911
  @proxify
912
  async switch(telephonySessionId: string) {
NEW
913
    try {
×
NEW
914
      this.setCallControlBusyTimestamp();
×
NEW
915
      await this.transferUnmuteHandler(telephonySessionId);
×
NEW
916
      const switchedSession = await this._rcCall!.switchCall(
×
917
        telephonySessionId,
918
        {
919
          homeCountryId: this._deps.regionSettings.homeCountryId,
920
        },
921
      );
NEW
922
      this._triggerAutoMergeEvent(telephonySessionId);
×
NEW
923
      await this._holdOtherCalls(telephonySessionId);
×
NEW
924
      this.clearCallControlBusyTimestamp();
×
NEW
925
      this._onCallSwitchedFunc?.(switchedSession.sessionId);
×
926
    } catch (error: any) {
927
      // TODO: fix error handling with instanceof
NEW
928
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
NEW
929
        this._deps.alert.warning({ message: callControlError.generalError });
×
930
      }
NEW
931
      this.clearCallControlBusyTimestamp();
×
932
    }
933
  }
934

NEW
935
  @track((that: ActiveCallControl) => [
×
936
    that._getTrackEventName(trackEvents.hold),
937
  ])
938
  @proxify
939
  async hold(telephonySessionId: string) {
940
    try {
1✔
941
      this.setCallControlBusyTimestamp();
1✔
942
      const session = this._getSessionById(telephonySessionId);
1✔
943
      const { webphoneSession, otherParties = [] } = session;
1!
944
      if (
1!
945
        // when call is connecting or in voicemail then call control's Hold API will not work
946
        // so use webphone hold here
947
        (session.direction === callDirection.outbound &&
4✔
948
          (otherParties[0]?.status?.code === PartyStatusCode.proceeding ||
949
            otherParties[0]?.status?.code === PartyStatusCode.voicemail)) ||
950
        isAtMainNumberPromptToneStage(session)
951
      ) {
NEW
952
        await webphoneSession.hold();
×
953
      } else {
954
        await session.hold();
1✔
955
      }
956
      if (webphoneSession && webphoneSession.__rc_callStatus) {
1!
NEW
957
        webphoneSession.__rc_callStatus = sessionStatus.onHold;
×
958
      }
959
      this.clearCallControlBusyTimestamp();
1✔
960
    } catch (error: any) {
961
      // TODO: fix error handling with instanceof
NEW
962
      if (error.response && !error.response._text) {
×
NEW
963
        error.response._text = await error.response.clone().text();
×
964
      }
NEW
965
      if (conflictError(error)) {
×
NEW
966
        this._deps.alert.warning({
×
967
          message: callControlError.holdConflictError,
968
        });
NEW
969
      } else if (
×
970
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
971
      ) {
NEW
972
        this._deps.alert.warning({ message: callControlError.generalError });
×
973
      }
NEW
974
      this.clearCallControlBusyTimestamp();
×
975
    }
976
  }
977

NEW
978
  @track((that: ActiveCallControl) => [
×
979
    that._getTrackEventName(trackEvents.unhold),
980
  ])
981
  @proxify
982
  async unhold(telephonySessionId: string) {
NEW
983
    try {
×
NEW
984
      this.setCallControlBusyTimestamp();
×
NEW
985
      const session = this._getSessionById(telephonySessionId);
×
NEW
986
      await this._holdOtherCalls(telephonySessionId);
×
NEW
987
      await session.unhold();
×
NEW
988
      this._activeSession = session;
×
NEW
989
      const { webphoneSession } = session;
×
NEW
990
      if (webphoneSession && webphoneSession.__rc_callStatus) {
×
NEW
991
        webphoneSession.__rc_callStatus = sessionStatus.connected;
×
992
      }
NEW
993
      this.setActiveSessionId(telephonySessionId);
×
NEW
994
      this._addTrackToActiveSession();
×
NEW
995
      this.clearCallControlBusyTimestamp();
×
996
    } catch (error: any) {
997
      // TODO: fix error handling with instanceof
NEW
998
      if (error.response && !error.response._text) {
×
NEW
999
        error.response._text = await error.response.clone().text();
×
1000
      }
NEW
1001
      if (conflictError(error)) {
×
NEW
1002
        this._deps.alert.warning({
×
1003
          message: callControlError.unHoldConflictError,
1004
        });
NEW
1005
      } else if (
×
1006
        !(await this._deps.availabilityMonitor?.checkIfHAError(error))
1007
      ) {
NEW
1008
        this._deps.alert.warning({
×
1009
          message: callControlError.generalError,
1010
        });
1011
      }
NEW
1012
      this.clearCallControlBusyTimestamp();
×
1013
    }
1014
  }
1015

1016
  @track((_, params: ReplyWithTextParams) => {
NEW
1017
    let messageType = 'End-User Custom Message';
×
NEW
1018
    if (params.replyWithPattern) {
×
NEW
1019
      const pattern = params.replyWithPattern?.pattern;
×
NEW
1020
      if (
×
1021
        pattern === ReplyWithPattern.inAMeeting ||
×
1022
        pattern === ReplyWithPattern.onMyWay
1023
      ) {
NEW
1024
        messageType = 'Default Static Message';
×
1025
      } else {
NEW
1026
        messageType = 'Default Dynamic Message';
×
1027
      }
1028
    }
NEW
1029
    return [
×
1030
      trackEvents.executionReplyWithMessage,
1031
      {
1032
        'Message Type': messageType,
1033
      },
1034
    ];
1035
  })
1036
  @proxify
1037
  async replyWithMessage(
1038
    params: ReplyWithTextParams,
1039
    telephonySessionId: string,
1040
  ) {
NEW
1041
    try {
×
NEW
1042
      this.setCallControlBusyTimestamp();
×
NEW
1043
      const session = this._getSessionById(telephonySessionId);
×
NEW
1044
      if (!session) {
×
NEW
1045
        return false;
×
1046
      }
NEW
1047
      await session.replyWithMessage(params);
×
NEW
1048
      this.clearCallControlBusyTimestamp();
×
NEW
1049
      this._deps.alert.success({ message: callControlError.replyCompleted });
×
1050
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1051
      console.error('replyWithMessage error', error);
×
NEW
1052
      this._deps.alert.warning({ message: callControlError.generalError });
×
NEW
1053
      this.clearCallControlBusyTimestamp();
×
1054
    }
1055
  }
1056

1057
  @proxify
1058
  async toVoicemail(voicemailId: string, telephonySessionId: string) {
NEW
1059
    try {
×
NEW
1060
      this.setCallControlBusyTimestamp();
×
NEW
1061
      const session = this._getSessionById(telephonySessionId);
×
NEW
1062
      if (!session) {
×
NEW
1063
        return false;
×
1064
      }
NEW
1065
      await session.transfer(voicemailId, { type: 'voicemail' });
×
NEW
1066
      this.clearCallControlBusyTimestamp();
×
NEW
1067
      this._deps.alert.success({ message: callControlError.transferCompleted });
×
1068
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1069
      console.error('toVoicemail error', error);
×
NEW
1070
      this._deps.alert.warning({ message: webphoneErrors.toVoiceMailError });
×
NEW
1071
      this.clearCallControlBusyTimestamp();
×
1072
    }
1073
  }
1074

1075
  @proxify
1076
  async completeWarmTransfer(telephonySession: string) {
NEW
1077
    try {
×
NEW
1078
      this.setCallControlBusyTimestamp();
×
1079

1080
      const { isOriginal, relatedTelephonySessionId } =
NEW
1081
        this.transferCallMapping[telephonySession];
×
1082

NEW
1083
      const session = this._getSessionById(
×
1084
        isOriginal ? telephonySession : relatedTelephonySessionId,
×
1085
      );
NEW
1086
      const transferSession = this._getSessionById(
×
1087
        isOriginal ? relatedTelephonySessionId : telephonySession,
×
1088
      );
1089

NEW
1090
      if (!transferSession) {
×
NEW
1091
        return false;
×
1092
      }
NEW
1093
      await session.warmTransfer(transferSession);
×
NEW
1094
      this.clearCallControlBusyTimestamp();
×
NEW
1095
      this._deps.alert.success({ message: callControlError.transferCompleted });
×
1096
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1097
      console.error('warmTransfer error', error);
×
NEW
1098
      this._deps.alert.warning({ message: callControlError.generalError });
×
NEW
1099
      this.clearCallControlBusyTimestamp();
×
1100
    }
1101
  }
1102

1103
  @track(trackEvents.transfer)
1104
  @proxify
1105
  async transfer(transferNumber: string, telephonySessionId: string) {
NEW
1106
    try {
×
NEW
1107
      this.setCallControlBusyTimestamp();
×
NEW
1108
      const session = this._getSessionById(telephonySessionId);
×
NEW
1109
      const phoneNumber = await this.getValidPhoneNumber(transferNumber, true);
×
NEW
1110
      if (phoneNumber) {
×
NEW
1111
        await session.transfer(phoneNumber);
×
NEW
1112
        this.clearCallControlBusyTimestamp();
×
NEW
1113
        this._deps.alert.success({
×
1114
          message: callControlError.transferCompleted,
1115
        });
1116
      }
1117
    } catch (error: any) {
1118
      // TODO: fix error handling with instanceof
NEW
1119
      if (!(await this._deps.availabilityMonitor?.checkIfHAError(error))) {
×
NEW
1120
        this._deps.alert.warning({ message: webphoneErrors.transferError });
×
1121
      }
NEW
1122
      this.clearCallControlBusyTimestamp();
×
1123
    }
1124
  }
1125

1126
  async getValidPhoneNumber(phoneNumber: string, withMainNumber?: boolean) {
1127
    let validatedResult;
1128
    let validPhoneNumber;
1129
    if (!this._permissionCheck) {
3✔
1130
      validatedResult = validateNumbers({
1✔
1131
        allowRegionSettings: !!this._deps.brand.brandConfig.allowRegionSettings,
1132
        areaCode: this._deps.regionSettings.areaCode,
1133
        countryCode: this._deps.regionSettings.countryCode,
1134
        phoneNumbers: [phoneNumber],
1135
      });
1136
      validPhoneNumber = validatedResult[0];
1✔
1137
    } else {
1138
      const isEDPEnabled = this._deps.appFeatures?.isEDPEnabled;
2✔
1139
      validatedResult = isEDPEnabled
2!
1140
        ? this._deps.numberValidate.validate([phoneNumber])
1141
        : await this._deps.numberValidate.validateNumbers([phoneNumber]);
1142

1143
      if (!validatedResult.result) {
2✔
1144
        validatedResult.errors.forEach(async (error) => {
1✔
1145
          const isHAError: boolean =
1146
            // @ts-expect-error
1147
            !!(await this._deps.availabilityMonitor?.checkIfHAError(error));
1✔
1148
          if (!isHAError) {
1!
1149
            // TODO: fix `callErrors` type
1150
            this._deps.alert.warning({
1✔
1151
              message: (callErrors as any)[error.type],
1152
              payload: {
1153
                phoneNumber: error.phoneNumber,
1154
              },
1155
            });
1156
          }
1157
        });
1158
        return;
1✔
1159
      }
1160
      if (isEDPEnabled) {
1!
NEW
1161
        const parsedNumbers = await this._deps.numberValidate.parseNumbers([
×
1162
          phoneNumber,
1163
        ]);
NEW
1164
        validPhoneNumber =
×
1165
          parsedNumbers?.[0].availableExtension ??
×
1166
          parsedNumbers?.[0].parsedNumber;
1167
      } else {
1168
        // TODO: fix `validatedResult` type in `numberValidate` module.
1169
        validPhoneNumber = (validatedResult as any).numbers?.[0]?.e164;
1✔
1170
      }
1171
    }
1172

1173
    let result = validPhoneNumber;
2✔
1174
    if (withMainNumber && validPhoneNumber.indexOf('+') === -1) {
2!
NEW
1175
      result = [
×
1176
        this._deps.accountInfo.mainCompanyNumber,
1177
        validPhoneNumber,
1178
      ].join('*');
1179
    }
1180

1181
    return result;
2✔
1182
  }
1183

1184
  // FIXME: Incomplete Implementation?
1185
  @proxify
1186
  async flip(flipValue: string, telephonySessionId: string) {
NEW
1187
    try {
×
NEW
1188
      this.setCallControlBusyTimestamp();
×
NEW
1189
      const session = this._getSessionById(telephonySessionId);
×
NEW
1190
      await session.flip({ callFlipId: flipValue });
×
NEW
1191
      this.clearCallControlBusyTimestamp();
×
1192
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1193
      console.error('flip error', error);
×
NEW
1194
      this.clearCallControlBusyTimestamp();
×
NEW
1195
      throw error;
×
1196
    }
1197
  }
1198

NEW
1199
  @track((that: ActiveCallControl) => [
×
1200
    that._getTrackEventName(trackEvents.confirmForward),
1201
  ])
1202
  @proxify
1203
  async forward(forwardNumber: string, telephonySessionId: string) {
1204
    const session = this._getSessionById(telephonySessionId);
2✔
1205
    if (!session) {
2!
NEW
1206
      return false;
×
1207
    }
1208
    try {
2✔
1209
      let validatedResult;
1210
      let validPhoneNumber;
1211
      if (!this._permissionCheck) {
2✔
1212
        validatedResult = validateNumbers({
1✔
1213
          // @ts-expect-error
1214
          allowRegionSettings: this._deps.brand.brandConfig.allowRegionSettings,
1215
          areaCode: this._deps.regionSettings.areaCode,
1216
          countryCode: this._deps.regionSettings.countryCode,
1217
          phoneNumbers: [forwardNumber],
1218
        });
1219
        validPhoneNumber = validatedResult[0];
1✔
1220
      } else {
1221
        const isEDPEnabled = this._deps.appFeatures?.isEDPEnabled;
1✔
1222
        validatedResult = isEDPEnabled
1!
1223
          ? this._deps.numberValidate.validate([forwardNumber])
1224
          : await this._deps.numberValidate.validateNumbers([forwardNumber]);
1225

1226
        if (!validatedResult.result) {
1!
1227
          validatedResult.errors.forEach((error) => {
1✔
1228
            this._deps.alert.warning({
1✔
1229
              message: (callErrors as any)[error.type],
1230
              payload: {
1231
                phoneNumber: error.phoneNumber,
1232
              },
1233
            });
1234
          });
1235
          return false;
1✔
1236
        }
NEW
1237
        if (isEDPEnabled) {
×
NEW
1238
          const parsedNumbers = await this._deps.numberValidate.parseNumbers([
×
1239
            forwardNumber,
1240
          ]);
NEW
1241
          if (parsedNumbers) {
×
NEW
1242
            validPhoneNumber =
×
1243
              parsedNumbers[0].availableExtension ??
×
1244
              parsedNumbers[0].parsedNumber;
1245
          }
1246
        } else {
NEW
1247
          validPhoneNumber = (validatedResult as any).numbers?.[0]?.e164;
×
1248
        }
1249
      }
1250
      if (session && session.webphoneSession) {
1!
NEW
1251
        session.webphoneSession.__rc_isForwarded = true;
×
1252
      }
1253

1254
      await session.forward(validPhoneNumber, this.acceptOptions);
1✔
NEW
1255
      this._deps.alert.success({
×
1256
        message: callControlError.forwardSuccess,
1257
      });
NEW
1258
      if (typeof this._onCallEndFunc === 'function') {
×
NEW
1259
        this._onCallEndFunc();
×
1260
      }
NEW
1261
      return true;
×
1262
    } catch (e: any /** TODO: confirm with instanceof */) {
1263
      console.error(e);
1✔
1264
      this._deps.alert.warning({
1✔
1265
        message: webphoneErrors.forwardError,
1266
      });
1267
      return false;
1✔
1268
    }
1269
  }
1270

1271
  // DTMF handing by webphone session temporary, due to rc call session doesn't support currently
1272
  @proxify
1273
  async sendDTMF(dtmfValue: string, telephonySessionId: string) {
NEW
1274
    try {
×
NEW
1275
      const session = this._getSessionById(telephonySessionId);
×
1276
      // TODO: using rc call session
NEW
1277
      const { webphoneSession } = session;
×
NEW
1278
      if (webphoneSession) {
×
NEW
1279
        await webphoneSession.dtmf(dtmfValue, 100);
×
1280
      }
1281
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1282
      console.log('send dtmf error', error);
×
NEW
1283
      throw error;
×
1284
    }
1285
  }
1286

1287
  private _onWebphoneInvite(session: WebphoneSession) {
NEW
1288
    const webphoneSession = session;
×
NEW
1289
    if (!webphoneSession) return;
×
NEW
1290
    if (!webphoneSession.__rc_creationTime) {
×
NEW
1291
      webphoneSession.__rc_creationTime = Date.now();
×
1292
    }
NEW
1293
    if (!webphoneSession.__rc_lastActiveTime) {
×
NEW
1294
      webphoneSession.__rc_lastActiveTime = Date.now();
×
1295
    }
NEW
1296
    webphoneSession.on('terminated', () => {
×
NEW
1297
      console.log('Call Event: terminated');
×
1298
      // this.setLastEndedSessionIds(webphoneSession);
1299
      const { telephonySessionId } =
NEW
1300
        this.rcCallSessions.find(
×
NEW
1301
          (s) => s.webphoneSession === webphoneSession,
×
1302
        ) || {};
1303

NEW
1304
      if (!telephonySessionId) return;
×
1305

NEW
1306
      this._setActiveSessionIdFromOnHoldCalls(telephonySessionId);
×
NEW
1307
      this._onCallEnd(telephonySessionId);
×
1308
    });
NEW
1309
    webphoneSession.on('accepted', () => {
×
1310
      const { telephonySessionId } =
NEW
1311
        this.rcCallSessions.find(
×
NEW
1312
          (s) => s.webphoneSession === webphoneSession,
×
1313
        ) || {};
1314

NEW
1315
      if (!telephonySessionId) return;
×
1316

NEW
1317
      if (this._autoMergeWebphoneSessionsMap.get(webphoneSession)) {
×
NEW
1318
        this._autoMergeWebphoneSessionsMap.delete(webphoneSession);
×
1319
      } else {
NEW
1320
        this.setActiveSessionId(telephonySessionId);
×
NEW
1321
        this._holdOtherCalls(telephonySessionId);
×
NEW
1322
        this._addTrackToActiveSession();
×
1323
      }
NEW
1324
      this.updateActiveSessions();
×
NEW
1325
      this._onCallAccepted(telephonySessionId);
×
1326
    });
1327
  }
1328

1329
  @action
1330
  private _setRingSessionId(sessionId: string | null = null) {
×
NEW
1331
    this.data.ringSessionId = sessionId;
×
1332
  }
1333

1334
  /**
1335
   *if current call is terminated, then pick the first onhold call as active current call;
1336
   *
1337
   * @param {Session} session
1338
   * @memberof ActiveCallControl
1339
   */
1340
  private _setActiveSessionIdFromOnHoldCalls(telephonySessionId: string) {
NEW
1341
    if (!telephonySessionId) return;
×
NEW
1342
    if (this.activeSessionId === telephonySessionId) {
×
NEW
1343
      const onHoldSessions: ActiveCallControlSessionData[] = filter(
×
NEW
1344
        (s: ActiveCallControlSessionData) => isHolding(s),
×
1345
        this.sessions,
1346
      );
NEW
1347
      if (onHoldSessions.length) {
×
NEW
1348
        this.setActiveSessionId(onHoldSessions[0].telephonySessionId);
×
1349
      }
1350
    }
1351
  }
1352

1353
  @proxify
1354
  private async _holdOtherCalls(telephonySessionId?: string) {
1355
    const currSessions = this._rcCall!.sessions! as Session[];
1✔
1356
    const otherSessions = filter((s) => {
1✔
1357
      return (
2✔
1358
        s.telephonySessionId !== telephonySessionId &&
3!
1359
        (s.status === PartyStatusCode.answered ||
1360
          (s.webphoneSession && !s.webphoneSession.localHold))
1361
      );
1362
    }, currSessions);
1363
    if (!otherSessions.length) {
1!
NEW
1364
      return;
×
1365
    }
1366
    const holdOtherSessions = otherSessions.map(async (session) => {
1✔
1367
      const { webphoneSession, otherParties = [] } = session;
1!
1368
      try {
1✔
1369
        if (
1!
1370
          // when call is connecting or in voicemail then call control's Hold API will not work
1371
          // so use webphone hold here
1372
          (session.direction === callDirection.outbound &&
4✔
1373
            (otherParties[0]?.status?.code === PartyStatusCode.proceeding ||
1374
              otherParties[0]?.status?.code === PartyStatusCode.voicemail)) ||
1375
          isAtMainNumberPromptToneStage(session)
1376
        ) {
1377
          await webphoneSession.hold();
1✔
1378
        } else {
NEW
1379
          await session.hold();
×
1380
        }
1381
        if (webphoneSession && webphoneSession.__rc_callStatus) {
1!
NEW
1382
          webphoneSession.__rc_callStatus = sessionStatus.onHold;
×
1383
        }
1384
      } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1385
        console.log('Hold call fail.', error);
×
1386
      }
1387
    });
1388
    await Promise.all(holdOtherSessions);
1✔
1389
  }
1390

1391
  @proxify
1392
  private async _answer(telephonySessionId: string) {
NEW
1393
    this._triggerAutoMergeEvent(telephonySessionId);
×
NEW
1394
    this.setCallControlBusyTimestamp();
×
NEW
1395
    const session = this._getSessionById(telephonySessionId);
×
1396

NEW
1397
    this._activeSession = session;
×
NEW
1398
    await this._holdOtherCalls(telephonySessionId);
×
NEW
1399
    const { webphoneSession } = session;
×
NEW
1400
    const deviceId = this._deps.webphone?.device?.id;
×
NEW
1401
    await session.answer({ deviceId });
×
NEW
1402
    this._trackWebRTCCallAnswer();
×
NEW
1403
    if (webphoneSession && webphoneSession.__rc_callStatus) {
×
NEW
1404
      webphoneSession.__rc_callStatus = sessionStatus.connected;
×
1405
    }
NEW
1406
    this.clearCallControlBusyTimestamp();
×
1407
  }
1408

NEW
1409
  @track((that: ActiveCallControl) => [
×
1410
    that._getTrackEventName(trackEvents.answer),
1411
  ])
1412
  @proxify
1413
  async answer(telephonySessionId: string) {
NEW
1414
    try {
×
NEW
1415
      await this._answer(telephonySessionId);
×
1416
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1417
      console.log('answer failed.');
×
1418
    }
1419
  }
1420

NEW
1421
  @track((that: ActiveCallControl) => [
×
1422
    that._getTrackEventName(trackEvents.holdAndAnswer),
1423
  ])
1424
  @proxify
1425
  async answerAndHold(telephonySessionId: string) {
1426
    // currently, the logic is same as answer
NEW
1427
    try {
×
NEW
1428
      await this._answer(telephonySessionId);
×
1429
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1430
      console.log('answer hold failed.', error);
×
1431
    }
1432
  }
1433

1434
  /**
1435
   * ignore an incoming WebRTC call, after action executed, call will be ignored at current
1436
   * device and move to "calls on other device" section. This call still can be answered at other
1437
   * device
1438
   * @param {string} telephonySessionId
1439
   * @memberof ActiveCallControl
1440
   */
NEW
1441
  @track((that: ActiveCallControl) => [
×
1442
    that._getTrackEventName(trackEvents.ignore),
1443
  ])
1444
  @proxify
1445
  async ignore(telephonySessionId: string) {
NEW
1446
    try {
×
NEW
1447
      this.setCallControlBusyTimestamp();
×
NEW
1448
      const session = this._getSessionById(telephonySessionId);
×
NEW
1449
      const { webphoneSession } = session;
×
NEW
1450
      await webphoneSession.reject();
×
1451
      // hack for update sessions, then incoming call log page can re-render
NEW
1452
      setTimeout(() => this.updateActiveSessions(), 0);
×
NEW
1453
      this.clearCallControlBusyTimestamp();
×
1454
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1455
      console.log('ignore failed.', error);
×
1456
    }
1457
  }
1458

NEW
1459
  @track((that: ActiveCallControl) => [
×
1460
    that._getTrackEventName(trackEvents.endAndAnswer),
1461
  ])
1462
  @proxify
1463
  async answerAndEnd(telephonySessionId: string) {
1464
    try {
1✔
1465
      if (this.busy) return;
1!
1466
      this.setCallControlBusyTimestamp();
1✔
1467
      const session = this._getSessionById(telephonySessionId);
1✔
1468
      const currentActiveCalls = this._rcCall!.sessions.filter(
1✔
1469
        (s) =>
1470
          s.id !== telephonySessionId &&
2✔
1471
          s.webphoneSession &&
1472
          (s.status === PartyStatusCode.answered ||
1473
            (s.direction === callDirection.outbound &&
1474
              s.status === PartyStatusCode.proceeding)),
1475
      );
1476
      for (const s of currentActiveCalls) {
1✔
1477
        await s.hangup();
1✔
1478
      }
1479
      const deviceId = this._deps.webphone?.device?.id;
1✔
NEW
1480
      await session.answer({ deviceId });
×
NEW
1481
      this._trackWebRTCCallAnswer();
×
NEW
1482
      const { webphoneSession } = session;
×
NEW
1483
      if (webphoneSession && webphoneSession.__rc_callStatus) {
×
NEW
1484
        webphoneSession.__rc_callStatus = sessionStatus.connected;
×
1485
      }
NEW
1486
      this.clearCallControlBusyTimestamp();
×
1487
    } catch (error: any /** TODO: confirm with instanceof */) {
1488
      console.log('answer and end fail.');
1✔
1489
      console.error(error);
1✔
1490
    }
1491
  }
1492

1493
  async startWarmTransfer(transferNumber: string, telephonySessionId: string) {
1494
    // todo handle error;
NEW
1495
    const toNumber = await this.getValidPhoneNumber(transferNumber);
×
NEW
1496
    return this.makeCall({
×
1497
      toNumber,
1498
      transferSessionId: telephonySessionId,
1499
    });
1500
  }
1501

1502
  @action
1503
  setWarmTransferMapping(originalId: string, transferredId: string) {
NEW
1504
    this.transferCallMapping = {
×
1505
      ...this.transferCallMapping,
1506
      [originalId]: {
1507
        relatedTelephonySessionId: transferredId,
1508
        isOriginal: true,
1509
      },
1510
      [transferredId]: {
1511
        relatedTelephonySessionId: originalId,
1512
        isOriginal: false,
1513
      },
1514
    };
1515
  }
1516

1517
  @action
1518
  cleanCurrentWarmTransferData() {
NEW
1519
    const warmTransferSessionIds = Object.keys(this.transferCallMapping);
×
NEW
1520
    const currentSessionIds = this.sessions.map(
×
NEW
1521
      (session) => session.telephonySessionId,
×
1522
    );
NEW
1523
    const needRemovedIds = warmTransferSessionIds.filter(
×
NEW
1524
      (telephonySessionId) => !currentSessionIds.includes(telephonySessionId),
×
1525
    );
1526

NEW
1527
    if (needRemovedIds.length > 0) {
×
NEW
1528
      const removeSessionSet = new Set(needRemovedIds);
×
1529

NEW
1530
      const filteredData = Object.fromEntries(
×
1531
        Object.entries(this.transferCallMapping).filter(
1532
          ([id, transferInfo]) =>
NEW
1533
            !(
×
1534
              removeSessionSet.has(id) ||
×
1535
              removeSessionSet.has(transferInfo.relatedTelephonySessionId)
1536
            ),
1537
        ),
1538
      );
1539

NEW
1540
      this.transferCallMapping = filteredData;
×
1541
    }
1542
  }
1543

1544
  @proxify
1545
  async makeCall(params: ModuleMakeCallParams) {
NEW
1546
    try {
×
NEW
1547
      if (
×
1548
        params.toNumber.length > 6 &&
×
1549
        (!this._deps.availabilityMonitor ||
1550
          !this._deps.availabilityMonitor.isVoIPOnlyMode)
1551
      ) {
NEW
1552
        const phoneLines = await this._fetchDL();
×
NEW
1553
        if (phoneLines.length === 0) {
×
NEW
1554
          this._deps.alert.warning({
×
1555
            message: webphoneErrors.noOutboundCallWithoutDL,
1556
          });
NEW
1557
          return null;
×
1558
        }
1559
      }
NEW
1560
      await this._holdOtherCalls();
×
NEW
1561
      const sdkMakeCallParams: MakeCallParams = {
×
1562
        // type 'callControl' not support webphone's sip device currently.
1563
        type: 'webphone',
1564
        toNumber: params.toNumber,
1565
        fromNumber: params.fromNumber,
1566
        homeCountryId: params.homeCountryId,
1567
      };
NEW
1568
      const session = (await this._rcCall!.makeCall(
×
1569
        sdkMakeCallParams,
1570
      )) as Session;
NEW
1571
      this._activeSession = session;
×
NEW
1572
      session.webphoneSession.on('progress', () => {
×
NEW
1573
        if (
×
1574
          session.telephonySessionId &&
×
1575
          this.activeSessionId !== session.telephonySessionId
1576
        ) {
NEW
1577
          this.setActiveSessionId(session.telephonySessionId);
×
1578

NEW
1579
          const { transferSessionId } = params;
×
NEW
1580
          if (transferSessionId) {
×
NEW
1581
            this.setWarmTransferMapping(
×
1582
              transferSessionId,
1583
              session.telephonySessionId,
1584
            );
1585
          }
1586
        }
1587
      });
NEW
1588
      this._triggerAutoMergeEvent();
×
NEW
1589
      return session;
×
1590
    } catch (error: any /** TODO: confirm with instanceof */) {
NEW
1591
      console.log('make call fail.', error);
×
1592
    }
1593
  }
1594

1595
  private async _fetchDL() {
NEW
1596
    const response = await this._deps.client
×
1597
      .account()
1598
      .extension()
1599
      .device()
1600
      .list();
NEW
1601
    const devices = response.records;
×
NEW
1602
    let phoneLines: any[] = [];
×
NEW
1603
    devices?.forEach((device) => {
×
1604
      // wrong type of phoneLines, temporary treat it as any
NEW
1605
      if (!device.phoneLines || (device.phoneLines as any).length === 0) {
×
NEW
1606
        return;
×
1607
      }
NEW
1608
      phoneLines = phoneLines.concat(device.phoneLines);
×
1609
    });
NEW
1610
    return phoneLines;
×
1611
  }
1612

1613
  getActiveSession(telephonySessionId: string | null) {
NEW
1614
    if (!telephonySessionId) {
×
NEW
1615
      return null;
×
1616
    }
NEW
1617
    return this.activeSessions[telephonySessionId];
×
1618
  }
1619

1620
  getSession(telephonySessionId: string) {
NEW
1621
    return this.sessions.find(
×
NEW
1622
      (session) => session.telephonySessionId === telephonySessionId,
×
1623
    );
1624
  }
1625

NEW
1626
  @computed(({ activeSessionId, activeSessions }: ActiveCallControl) => [
×
1627
    activeSessionId,
1628
    activeSessions,
1629
  ])
1630
  get activeSession() {
NEW
1631
    return this.getActiveSession(this.activeSessionId);
×
1632
  }
1633

NEW
1634
  @computed(({ ringSessionId, activeSessions }: ActiveCallControl) => [
×
1635
    ringSessionId,
1636
    activeSessions,
1637
  ])
1638
  get ringSession() {
NEW
1639
    return this.getActiveSession(this.ringSessionId);
×
1640
  }
1641

NEW
1642
  @computed(({ sessions }: ActiveCallControl) => [sessions])
×
1643
  get ringSessions() {
NEW
1644
    if (!this.sessions) {
×
NEW
1645
      return [];
×
1646
    }
NEW
1647
    return this.sessions.filter((session: ActiveCallControlSessionData) =>
×
NEW
1648
      isRinging(session),
×
1649
    );
1650
  }
1651

NEW
1652
  @computed((that: ActiveCallControl) => [that.sessions, that.timestamp])
×
1653
  get activeSessions() {
NEW
1654
    return this.sessions.reduce((accumulator, session) => {
×
NEW
1655
      const { id } = session;
×
NEW
1656
      accumulator[id!] = normalizeSession({ session });
×
NEW
1657
      return accumulator;
×
1658
    }, {} as Record<string, Partial<ActiveSession>>);
1659
  }
1660

NEW
1661
  @computed((that: ActiveCallControl) => [that._deps.presence.calls])
×
1662
  get sessionIdToTelephonySessionIdMapping() {
NEW
1663
    return this._deps.presence.calls.reduce((accumulator, call) => {
×
NEW
1664
      const { telephonySessionId, sessionId } = call;
×
NEW
1665
      accumulator[sessionId!] = telephonySessionId!;
×
NEW
1666
      return accumulator;
×
1667
    }, {} as Record<string, string>);
1668
  }
1669

1670
  /**
1671
   * Mitigation strategy for avoiding 404/409 on call control endpoints.
1672
   * This should gradually move towards per session controls rather than
1673
   * a global busy timeout.
1674
   */
1675
  get busy() {
NEW
1676
    return Date.now() - this.busyTimestamp < DEFAULT_BUSY_TIMEOUT;
×
1677
  }
1678

1679
  // This should reflect on the app permissions setting in DWP
1680
  get hasPermission() {
NEW
1681
    return this._deps.appFeatures.hasCallControl;
×
1682
  }
1683

1684
  get timeToRetry() {
NEW
1685
    return this._timeToRetry;
×
1686
  }
1687

1688
  get ttl() {
NEW
1689
    return this._ttl;
×
1690
  }
1691

1692
  get acceptOptions() {
NEW
1693
    return {
×
1694
      sessionDescriptionHandlerOptions: {
1695
        constraints: {
1696
          audio: {
1697
            deviceId: this._deps.audioSettings?.inputDeviceId,
1698
          },
1699
          video: false,
1700
        },
1701
      },
1702
    };
1703
  }
1704

1705
  get hasCallInRecording() {
NEW
1706
    return this.sessions.some((session) => isRecording(session));
×
1707
  }
1708

1709
  // TODO:refactor, use this.sessions instead
1710
  get rcCallSessions() {
NEW
1711
    return filter(
×
NEW
1712
      (session) => filterDisconnectedCalls(session),
×
1713
      this._rcCall?.sessions || [],
×
1714
    );
1715
  }
1716

1717
  get activeSessionId() {
NEW
1718
    return this.data.activeSessionId;
×
1719
  }
1720

1721
  get busyTimestamp() {
NEW
1722
    return this.data.busyTimestamp;
×
1723
  }
1724

1725
  get timestamp() {
NEW
1726
    return this.data.timestamp;
×
1727
  }
1728

1729
  get sessions() {
NEW
1730
    return this.data.sessions;
×
1731
  }
1732

1733
  get ringSessionId() {
NEW
1734
    return this.data.ringSessionId;
×
1735
  }
1736

1737
  @track(trackEvents.inboundWebRTCCallConnected)
1738
  _trackWebRTCCallAnswer() {}
1739

1740
  @track(trackEvents.dialpadOpen)
1741
  dialpadOpenTrack() {}
1742

1743
  @track(trackEvents.dialpadClose)
1744
  dialpadCloseTrack() {}
1745

NEW
1746
  @track((that: ActiveCallControl) => [
×
1747
    that._getTrackEventName(trackEvents.clickTransfer),
1748
  ])
1749
  clickTransferTrack() {}
1750

NEW
1751
  @track((that: ActiveCallControl) => [
×
1752
    that._getTrackEventName(trackEvents.forward),
1753
  ])
1754
  clickForwardTrack() {}
1755

1756
  @track((that: ActiveCallControl, path: string) => {
NEW
1757
    return (analytics) => {
×
1758
      // @ts-expect-error
NEW
1759
      const target = analytics.getTrackTarget();
×
NEW
1760
      return [
×
1761
        trackEvents.openEntityDetailLink,
1762
        { path: path || target.router },
×
1763
      ];
1764
    };
1765
  })
1766
  openEntityDetailLinkTrack(path: string) {}
1767

NEW
1768
  @track((that: ActiveCallControl) => [
×
1769
    that._getTrackEventName(trackEvents.switch),
1770
  ])
1771
  clickSwitchTrack() {}
1772

1773
  private _getSessionById(sessionId: string) {
1774
    const session = this._rcCall!.sessions.find((s) => s.id === sessionId);
4✔
1775

1776
    return session as Session;
4✔
1777
  }
1778
}
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