@tenfold/web-client-sdk
v1.0.42
Published
Tenfold Web Client SDK
Downloads
176
Readme
Tenfold SDK Client Library
Use this package to utilize tenfold features and build your own integration app / UI on top of it. The package is designed to be used in browser alike environments (window object required).
Installation guide
Node
Node - defined in package.json
- Node.js 20.11.1
You can use nvm
or fnm
to install this version.
Rxjs
There is a single peer dependency -> rxjs. Usage of this package requires knowledge of rxjs (https://rxjs-dev.firebaseapp.com/guide/overview) and concept of Observables.
Typescript support
Tenfold SDK Client Library is written with Typescript. This package includes TypeScript declarations for the Tenfold SDK Client Library. We support projects using TypeScript versions >= 4.1.
Installation
Use npm
or yarn
to install the Tenfold SDK Client Library:
> npm install @tenfold/web-client-sdk
or
> yarn add @tenfold/web-client-sdk
Architecture overview
To utilize Tenfold SDK Client Library you will need to instantiate a WebClient. It appends to DOM an extra iFrame that encapsulates most of the logic and state and communicates with this iFrame to dispatch actions and listen for data changes via rxjs observables.
Usage
WebClient
To start using Tenfold SDK Client Library you will need to use WebClient.
import { WebClient as TenfoldWebClient } from "@tenfold/web-client-sdk";
const webClient = new TenfoldWebClient({
sharedWorker: false,
iframeDivId: "some-sdk-custom-id",
sdkUrl: "https://app.tenfold.com/sdk.html",
});
<html>
<head>
...
</head>
<body>
<div id="some-sdk-custom-id"></div>
</body>
</html>
WebClient configuration
When you instantiate WebClient you may pass an optional configuration object. All these properties are optional.
Configuration sdkUrl
option (optional)
As described in Architecture overview
, WebClient requires an iFrame to communicate with.
As a default value we pass https://app.tenfold.com/v${sdkVersion}-sdk/sdk.html
. As Tenfold,
we provide per each sdk version matching iframe.src
. So if you have
@[email protected]
and you don't define this config property, WebClient should
attach to your DOM a div
with an iframe
that src
is https://app.tenfold.com/v1.0.24-sdk/sdk.html
. This may be exposed as a
configuration property for testing purposes but this is not recommended. Latest version is hosted at
https://app.tenfold.com/sdk.html
.
Configuration iframeDivId
option (optional)
You can pass id
attribute value of your div in HTML. The default value is __tenfold__sdk__iframe__
. You can only define <div/>
tags for this purpose in your code
to add an iFrame to dedicated <div/>
in DOM. For both - default or custom provided iframeDivId
- WebClient will look into the DOM and if it finds <div/>
with iframeDivId
value it will insert iframe with src
attribute and sdkUrl
value. If it doesn't find a <div/>
it will create one and prepend to document.body
and then insert an iFrame.
Configuration sharedWorker
option (optional)
Use a shared worker (SW) (https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) for underlying mechanism in iFrame. Default value is true
.
Shared worker is not implemented in all browsers, so even if this
configuration option is enabled we cannot ensure that it will bootstrap in this mode.
WebClient will fallback to non shared worker
mode in that case.
This mechanism gives a better UX in your app and better performance. Entire logic and state management in iFrame is bootstrapped only once and iFrame is only a proxy to SW. When SW is disabled, each iFrame bootstraps it's own instance of logic and state, so the UX accross many tabs in browser will not be exact the same. We recommend to check the behavior of your implementation with both values.
Configuration sharedWorkerContext
option (optional)
Only applies when the Shared Worker (SW) is enabled and implemented in the runtime environment. It defines a context for SW, so many diffrent apps using this SDK will not attach
to the same SW. As a default it's sdk_${window.location.origin}
. So different apps (under diffrent domains) should not attach to the same SW. You can define your own SW context by passing this option. In Chrome you can inspect your SWs under chrome://inspect/#workers.
WebClient methods and observables
WebClient destroy
When you create a new instance of WebClient
, it's important to remember to destroy it too.
If you create it once for entire life of your app you can skip this, however, if you create it
in some module and your intention is to clean it up when you destroy that module - use destroy
method. It ensures that all inner rxjs subscriptions will be completed and iframe
injected into given div
will be removed from DOM. Your code should look like:
yourDestroyFn() {
if (this.webClient) {
this.webClient.destroy();
this.webClient = null;
}
}
WebClient isReady$
It's an observable that will notify you that everything is established and works
(communication with iframe i.e.). You should wait with any manipulations via WebClient
until isReady$
emits true
value.
webClient.isReady$
.pipe(
takeUntil(this.isDestroyed$),
filter((isReady) => !!isReady),
tap(() => {
console.log("WebClient and Iframe are ready!");
})
)
.subscribe();
WebClient isAuthenticated$
It's an observable that will notify you that user is authenticated to Tenfold.
webClient.isAuthenticated$
.pipe(
takeUntil(this.isDestroyed$),
filter((isAuthenticated) => !!isAuthenticated),
tap(() => {
console.log("User is authenticated to Tenfold!");
})
)
.subscribe();
WebClient domain services
WebClient has several domain services
. Those are responsible for dispatching some actions to iframe (via methods) and listen for data state changes from iframe (via rxjs observables).
All of the domain services
are only available after the isReady$
observable emits true
value, before that those will be undefined
.
WebClient.env domain service
It exposes isReady$
observable that indicates iframe's services to be ready when emits true.
WebClient.isReady$
is better to use, cause ensures that both sides of communication are ready
to use. Exposes also setEnvironment
mainly to set environmentUrl that we want to use to communicate with Tenfold APIs (for switching between dev and prod envs). setEnvironment
method
can be used only when isReady$
emits true
value and user is authenticated - otherwise will
throw an error. currentEnvironment$
exposes observable with a current state of env configuration - the most important property of this state is environmentUrl
of course, which
indicates which one is currently set.
webClient.env.currentEnvironment$
.pipe(
takeUntil(this.isDestroyed$),
map(({ environmentUrl }) => environmentUrl),
tap((environmentUrl) => {
console.log("environmentUrl changed to:", environmentUrl);
})
)
.subscribe();
webClient.env.setEnvironment({ environmentUrl: "https://api.tenfold.com" });
WebClient.auth domain service
You can listen for user data (can be User or undefined
depends on WebClient.isAuthenticated$
value):
webClient.auth.user$
.pipe(
takeUntil(this.isDestroyed$),
filter((user) => !!user),
map(({ username }) => username),
tap((username) => {
console.log("user name is:", username);
})
)
.subscribe();
You can also get phoneSystem$
returns string or null
(just a sugar selector on top of user$
one):
webClient.auth.phoneSystem$
.pipe(
takeUntil(this.isDestroyed$),
filter((phoneSystem) => !!phoneSystem),
tap((phoneSystem) => {
console.log("Your phone system is:", phoneSystem);
})
)
.subscribe();
It exposes login
method for login with credentials:
await webClient.auth.login({
username: "[email protected]",
password: "********",
});
You can logout at any time:
await webClient.auth.logout();
You can also implement SSO flow:
const identifier = "[email protected]";
const loginType = "openid_flow"; // 'saml_flow' | 'openid_flow';
webClient.auth
.startSSOFlow(identifier, loginType)
.pipe(
tap(({ redirectTo }) => {
window.open(redirectTo);
})
)
.subscribe();
startSSOFlow(identifier, loginType)
takes two params.
identifier
is SSO domain or username. loginType
is "saml_flow" when you want to perform domain login, "openid_flow" when you want to
perform open id connect login. Underneath it starts long polling for
user authentication status change (it stops when user becomes authenticated or call startSSOFlow once again).
WebClient.agentStatus domain service
In login flow sometimes not only tenfold authentication is required - you need to
perform a login specific for a given phone system. You can do this with login
method:
await webClient.agentStatus.login({
agentId,
password,
extension,
});
To perform that you can check for getAgentSettings
to get some data to choose from:
webClient.agentStatus
.getAgentSettings()
.pipe(
takeUntil(this.isDestroyed$),
tap((agentSettings) => {
console.log("Your prefill agent id is:", agentSettings?.agentId);
console.log(
"If your hasPassword is truthy you can set your password to '▪▪▪▪▪▪▪▪'",
agentSettings?.hasPassword
);
console.log(
"Your preferredExtension is:",
agentSettings?.preferredExtension
);
})
)
.subscribe();
After you login you can implement widget with agent status selection:
webClient.agentStatus.agentStatuses$
.pipe(
takeUntil(this.isDestroyed$),
tap((agentStatuses) => {
console.log(
"Your options list of agentStatuses is:",
agentStatuses
);
})
)
.subscribe();
webClient.agentStatus.currentAgentStatus$
.pipe(
takeUntil(this.isDestroyed$),
tap((currentAgentStatus) => {
console.log("Your current agentStatus is:", currentAgentStatus);
})
)
.subscribe();
const agentStatusIJustChose = {
id: "some-id-of-option-from-above-agentStatuses",
type: "busy",
};
if (!agentStatusIJustChose.readOnly) {
await webClient.agentStatus.save(agentStatusIJustChose);
}
You can always get current agent login data with getAgentData
and use it for logout
.
const agentData = await webClient.agentStatus.getAgentData();
webClient.agentStatus.logout(agentData);
IMPORTANT: to implement this flow you will need some parts from WebClient.callControls
.
WebClient.interaction domain service
In Tenfold we have an Interaction
that is a common type for call, chat, etc. To handle data related to this important scope we have
WebClient.interaction
domain service.
You can listen for interactions collection changes with webClient.interaction.interactions$
observable. There is also webClient.interaction.newInteraction$
that emits a new Interaction
only when
it's new one in webClient.interaction.interactions$
. We expose also
a sugar observable on top of webClient.interaction.newInteraction$
that distinguish call and chat accordingly: webClient.interaction.newCall$
and webClient.interaction.newChat$
.
We also track changes and in case if any interaction in collection change we emit a value via webClient.interaction.interactionChange$
.
It's suitable to handle changes of each one in time.
There is also a sugar observable webClient.interaction.interactionStatusChange$
that emits { id: string, status: InteractionStatus }
when for any Interaction
from collection its
status
has changed.
webClient.interaction.interactions$
.pipe(
takeUntil(this.isDestroyed$),
tap((interactions) => {
console.log(
"Here is my always up to date list of latest interactions:",
interactions
);
})
)
.subscribe();
WebClient.callControls domain service
You can check with webClient.callControls.callControlsEnabled$
observable if
call controls for your agent are enabled.
To check if you have an agent with phone system that requires login you can
use webClient.callControls.hasSessionCreationPhoneSystem$
observable or webClient.callControls.hasSessionCreationPhoneSystem
getter.
To check if you have an agent that requires login you can
use webClient.callControls.hasAgentLogin$
observable or webClient.callControls.hasAgentLogin
getter.
The easiest and most safe solution to check if login is required (as combination of two above) is to use webClient.callControls.hasSessionCreation$
observable or webClient.callControls.hasSessionCreation
getter. This one is the one you need to
implement the proper agent login flow. Check: WebClient.agentStatus
domain service.
To check if the session is active you can use webClient.callControls.sessionActive$
observable or webClient.callControls.hasSessionActive
getter.
The easiest way to logout on agent level is to use webClient.callControls.destroySession
. It will logout you from agent login session, but not from Tenfold.
If you have webClient.callControls.hasSessionActive
truthy and you would like to
perform full logout:
await webClient.callControls.logout();
await webClient.auth.logout();
webClient.callControls
has a bunch of methods responsible for this domain like:
dial(phoneNumber: string, source: CTDRequestSource, pbxOptions?: DialPbxOptions)
with a parameters to setup any dial you want like:
const phoneNumber = "123456789";
webClient.callControls.dial(phoneNumber, "dialer", { type: "external" });
As name of this service suggests it's oriented on Interaction
of call type.
By tracking webClient.interaction.newCall$
you can
answerCall(call: Call)
,hangupCall(call: Call, force?: boolean)
,switchCall(call: Call)
,holdCall(call: Call)
,muteCall(call: Call)
,unmuteCall(call: Call)
,startRecording(call: Call)
,stopRecording(call: Call)
,retrieveCall(call: Call)
,sendDtmf(call: Call, sequence: string)
.
All above are self explanatory.
WebClient.features domain service
This domain service is responsible for checking availability for features defined in Tenfold. Depends on organization you belong and account configuration you can have access to only part of features.
The most basic observable to check tenfold feature setting is onFeature
.
webClient.features
.onFeature("socialProfile")
.pipe(
takeUntil(this.isDestroyed$),
tap((feature: Feature) => {
this.socialProfilesEnabled = feature && feature.status === "active";
})
)
.subscribe();
Sugar observable for status
extraction is onFeatureStatus
observable.
Sugar observable for property chain of feature is onFeaturePreference<T>(featureName: string, path: string, defaultValue: T)
observable.
Features that are user related can be found under onUserFeature
.
Features that are organization related can be found under onOrganizationFeature
.
On phone system level to check for feature use: webClient.features.onIntegrationCapability
.
Sugar observable for supports
extraction is: webClient.features.onIntegrationCapabilitySupport
.
Combination of onFeatureStatus
and onIntegrationCapabilitySupport
is: onIntegrationCapabilityAndFeatureSupport
(logical AND).
WebClient.agentDirectory domain service
We expose agentDirectory
to let navigate through tree of agent-alike objects.
Method fetchAgentDirectory(search: string)
is for getting a config object that you can
utilize to navigate through the tree. search
param is initial value for search$
pusher.
Returned config has also result$
and fetchPath$
.
const directoryConfig = webClient.agentDirectory.fetchAgentDirectory('');
directoryConfig.results$.pipe(
takeUntil(this.isDestroyed$),
tap((results) => {
console.log(
"Fresh results list for current search$ and fetchPath$ values:",
results
);
})
).subscribe();
clickOnAgentItem(item: AgentDirectoryItem) {
directoryConfig.fetchPath$.next(item.childrenPath);
}
directoryConfig.search$.next('John Smith');
This one is helpful for usage of transfers (see next section).
WebClient.transfers domain service
This domain service
supports concept of transfers
in Tenfold.
You should use it when you want implement a logic, that your agent is on call
with some customer and want to:
- redirect this call to another agent (without asking him about it first).
- switch current call to hanging mode, consult it with another agent and then discuss it with that agent, merge a call to join customer or return to customer.
No matter which scenario you want to implement, you need to set target agent that you want
transfer in-progress call to. For this purpose use webClient.transfers.setAgent
.
selectAgent(result: AgentDirectoryItem) {
const transferAgent = {
name: result.label,
agentStatus: result?.data?.state,
pointsOfContact: result.pointsOfContact,
hasPrimaryExtension: false,
} as TransferAgent;
await this.connectorService.getSDKService().transfers.setAgent(this.interaction.id, transferAgent);
}
The above code is for case when you select agent after using webClient.agentDirectory.fetchAgentDirectory
.
For scenario 1. you just need to use webClient.transfers.blindTransfer
. It will redirect call
to selected agent and end it with you.
For scenaro 2. you want to use webClient.transfers.handleInternalCall
.
- Then when you're on call with second agent and you can return to customer (ending a call with second agent) - use the same method
webClient.transfers.handleInternalCall
. - After discussion with second agent you may want to decide to join customer. You can do it with
webClient.transfers.mergeTransfer
. - If you want to complete this call with all parties just use
webClient.transfers.completeWarmTransfer
. - If you're no longer needed on the call with customer and second agent - just leave them on the call with
webClient.transfers.handOverCall
. - If you want jump from customer and second agent to be a middle-man you can just use
webClient.callControls.switchCall
. It works like toggle mechanism from one to another.
webClient.transfers.transfers$
is an observable that exposes state of all transfers as map
where key is Interaction.id
and value InteractionTransferState
. It keeps a state with data like:
agent
, internalCall
and transferStage
.
If you focus on single interaction that is in progress you might be intrested in that kind of extraction of transfer state for your interaction:
interactionTransferSubState$() {
return combineLatest([
this.interaction$,
this.transfers$,
]).pipe(
takeUntil(this.destroySubject$),
map(([call, transfers]) => {
return !!call ? transfers[call.id] : null;
}),
);
}