npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@tenfold/web-client-sdk

v1.0.42

Published

Tenfold Web Client SDK

Downloads

90

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:

  1. redirect this call to another agent (without asking him about it first).
  2. 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.

  1. 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.
  2. After discussion with second agent you may want to decide to join customer. You can do it with webClient.transfers.mergeTransfer.
  3. If you want to complete this call with all parties just use webClient.transfers.completeWarmTransfer.
  4. If you're no longer needed on the call with customer and second agent - just leave them on the call with webClient.transfers.handOverCall.
  5. 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;
      }),
    );
  }