Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/building/flex-hooks/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum FlexComponent {
QueueStats = 'QueueStats',
SideNav = 'SideNav',
SupervisorTaskCanvasHeader = 'SupervisorTaskCanvasHeader',
TaskCanvas = 'TaskCanvas',
TaskCanvasHeader = 'TaskCanvasHeader',
TaskCanvasTabs = 'TaskCanvasTabs',
TaskCard = 'TaskCard',
Expand Down
1 change: 1 addition & 0 deletions docs/docs/feature-library/00_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ The **Flex Project Template** comes with a set of features enabled by default wi
| [Omni Channel Management](omni-channel-capacity-management) | _method for mixing chat and voice channels_ | |
| [Queues Stats Metrics](queues-stats-metrics) | _add custom metrics columns to the Queues View_ | |
| [Ring Notification](ring-notification) | _plays a ringtone sound for incoming tasks_ | |
| [Salesforce Integration](salesforce-integration) | _example starting point for a custom Salesforce integration_ | |
| [Scrollable Activities](scrollable-activities) | _allow the scrolling of the activities list_ | |
| [SIP Support](sip-support) | _adds call control functionality when using a non-WebRTC phone_ | |

Expand Down
134 changes: 134 additions & 0 deletions docs/docs/feature-library/salesforce-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
sidebar_label: salesforce-integration
title: salesforce-integration
---

## Overview

This feature provides an enhanced Salesforce integration which replaces the out-of-box Salesforce integration plugin. It can be used as a starting point for customizing Salesforce integration functionality, which is not possible when using the out-of-box integration.

Functionality included within this implementation:
- Activity logging
- Creates activity upon task completion or cancellation
- Includes agent copilot disposition and summary
- Relates the activity to the dialed or screen-popped record, or the agent-selected record
- Click-to-dial
- Screen pop
- UI enhancements
- Disables the pop-out and pop-in buttons while on a call, to prevent accidental call hangups
- Hides the CRM container when embedded
- Hides the Flex logo in the header to make room for additional buttons
- Opens the Flex panel automatically when click-to-dial is used or an inbound task is received
- Updates the utility bar item icon and label with the worker's current activity (if no tasks), incoming tasks, or the number of active tasks
- When screen pop returns multiple records, a dropdown is added to the interface for the agent to select the appropriate record for activity logging

---

Example screenshot of handling an inbound call with UI enhancements and multiple screen pop results:

![Salesforce integration screenshot](/img/features/salesforce-integration/salesforce-integration.png)

---

## Business Details

### Context

Flex includes a Salesforce integration out-of-the-box, however, it is not fully customizable. If the out-of-box integration does not fully meet your needs, you may end up needing to build your own enhanced integration, re-creating the functionality included in the out-of-box integration.

### Objective

This `salesforce-integration` feature aims to be used as a starting point for your own customized Salesforce integration. The feature offers largely the same baseline functionality of the out-of-box integration, as well as some critical usability enhancements:

- Disables the pop-out and pop-in buttons while on a call, to prevent accidental call hangups
- Interface enhancements that streamline the agent workflow
- When screen pop returns multiple records, a dropdown is added to the interface for the agent to select the appropriate record for activity logging

### Configuration options

The feature is functional only when Flex is embedded within Salesforce as described [in the Flex documentation](https://www.twilio.com/docs/flex/admin-guide/integrations/salesforce). If the out-of-box Salesforce integration has been [enabled within the Twilio Console](https://console.twilio.com/us1/develop/flex/settings/integrations/salesforce), it must first be disabled.

To enable the feature, under the `flex-config` attributes set the `salesforce_integration` `enabled` flag to `true`.

```json
"salesforce_integration": {
"enabled": true,
"activity_logging": true, // Enables the automatic creation of activity records when a task is completed or canceled
"click_to_dial": true, // Enables handling click-to-dial within Salesforce
"copilot_notes": true, // Adds agent copilot disposition and summary to activity records created by the feature
"hide_crm_container": true, // Hides the Flex CRM container when embedded within Salesforce
"prevent_popout_during_call": true, // Disables the pop-out or pop-in button while on a call, to prevent accidental hangups
"screen_pop": true, // Enables search and screen pop of Salesforce records based on the inbound task attributes
"show_panel_automatically": true, // Pops open Flex when a task is received or click-to-dial is performed
"utility_bar_status": true // Updates the utility bar item icon and label with the current activity (if no tasks), incoming task, or number of active tasks
}
```

#### Screen pop attributes

When an inbound task is accepted, and the `screen_pop` configuration option is set to `true`, the feature will use task attributes in conjunction with the configured [Salesforce softphone layout](https://help.salesforce.com/s/articleView?id=service.cti_admin_phonelayouts.htm&type=5) to determine what record or page is displayed to the agent. Task attributes are used as follows:

1. If the `sfdcObjectId` attribute is present, the Salesforce record ID contained within the attribute will be popped. No other attributes will be used for screen pop when this attribute is present.
1. This can be useful when you are performing a data dip to find a record as part of the IVR and want to pop the same record.
1. Otherwise, a search within Salesforce will be performed, per the softphone layout, using the following task attributes in the following order:
1. `name`
1. `from`
1. `identity`
1. `customerAddress`

## Technical Details

The integration uses the [Salesforce Open CTI APIs](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_intro.htm) and the [Lightning Console API](https://developer.salesforce.com/docs/atlas.en-us.api_console.meta/api_console/sforce_api_console_js_getting_started.htm) to communicate with the Salesforce instance Flex is embedded within.

### Initialization

**File: `utils/SfdcLoader.ts`**

**Flex hook: `pluginsInitialized` event**

Before any integration functionality can be realized, we must first load the appropriate JS libraries from Salesforce. To do so, we load the Open CTI and Console API JS libraries from the Salesforce domain we are embedded within by inserting them as `<script>` elements in the DOM, if they are not already.

### Activity logging

**File: `utils/LogActivity.ts`**

**Flex hooks: `CompleteTask` action, `notesSubmitted` and `taskCanceled` event**

Activity logging is achieved by calling the [OpenCTI `saveLog()` function](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_savelog_lex.htm) to create a Task record in Salesforce after the agent has completed their handling of the task. This occurs when the agent presses the "Complete" button to complete the task, or when the task that they are handling is canceled (such as by a supervisor or by a failed outbound call).

If agent copilot functionality is enabled, this feature will collect the notes submitted using the `notesSubmitted` event, and will include them in the payload to `saveLog()`. As Salesforce does not include Activity fields out-of-the-box for all copilot data points, such as topics and sentiment, these are not submitted by the feature as-is. However, you may update the payload as desired to pass these into custom fields.

In order to correctly relate the activity log to another record, the `WhatId` or `WhoId` will be passed to `saveLog` based on the value of the task attribute `sfdcObjectId`. This attribute is set automatically by the click-to-dial functionality for outbound tasks, and is also set automatically by the screen pop functionality for inbound tasks when there is a single record match returned by Salesforce. If screen pop returned multiple matches, the user is presented with a dropdown menu in their task canvas, which sets the attribute when a record is selected. The `sfdcObjectType` attribute is used to store the type of object referenced by `sfdcObjectId`, which is then used to determine if the record ID should be passed as the `WhoId` (Contact or Lead) or the `WhatId` (everything else).

:::info Multiple match results handling

As described above, in order to determine the record to relate the activity log to, the user is presented with a dropdown list of records returned from the search results. However, it may be more desirable to instead integrate the selection within the Salesforce interface itself. One approach for achieving this is to use a Lightning Message Service (LMS) channel to publish a message that the Flex plugin can subscribe to and receive. Documentation for subscribing to an LMS channel via the Open CTI library [is available here](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_methods_lms.htm).

:::

### Click-to-dial

**File: `utils/ClickToDial.ts`**

**Flex hooks: `StartOutboundCall` action, `pluginsInitialized` event**

Click-to-dial is enabled by calling the [OpenCTI `enableClickToDial()` function](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_enableclicktodial_lex.htm) when the feature initializes, and if successful, passing a callback function to the [OpenCTI `onClickToDial()` function](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_onclicktodial_lex.htm) for Salesforce to execute when the user clicks on a number to dial.

When performing a click-to-dial, the callback function passed to `onClickToDial()` invokes the `StartOutboundCall` Flex Action to initiate the outbound call. In addition, the [OpenCTI `setSoftphonePanelVisibility()` function](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_setsoftphonepanelvisibility_lex.htm) is called to pop open the Flex utility bar item.

### Screen pop

**File: `utils/ScreenPop.ts`**

**Flex hook: `AcceptTask` action**

Screen pop is performed whenever the `AcceptTask` action is successfully invoked for inbound tasks. If there is already an `sfdcObjectId` attribute stored on the task, the feature will call the [OpenCTI `screenPop()` function](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_screenpop_lex.htm) to open the record ID specified in this attribute.

If there is no `sfdcObjectId` attribute stored on the task, the feature will call the [OpenCTI `searchAndScreenPop()` function](https://developer.salesforce.com/docs/atlas.en-us.api_cti.meta/api_cti/sforce_api_cti_searchandscreenpop_lex.htm), passing to it one of the following task attributes in the following order of precedence:

1. `name`
1. `from`
1. `identity`
1. `customerAddress`

Salesforce will then perform a search and screen pop based on the configured [softphone layout settings](https://help.salesforce.com/s/articleView?id=service.cti_admin_phonelayouts.htm&type=5). If a single match result is returned, that record will be saved to task attributes. If multiple match results are found, and activity logging is also enabled, a dropdown will be added to the task canvas for the agent to select the appropriate record from the search results. When selected, the record will be saved to task attributes. The task attributes will then be used for saving associations as part of activity logging.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions flex-config/ui_attributes.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,17 @@
"enabled": false,
"exclude_attributes": [],
"exclude_queues": []
},
"salesforce_integration": {
"enabled": false,
"activity_logging": true,
"click_to_dial": true,
"copilot_notes": true,
"hide_crm_container": true,
"prevent_popout_during_call": true,
"screen_pop": true,
"show_panel_automatically": true,
"utility_bar_status": true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getFeatureFlags } from '../../utils/configuration';
import SalesforceIntegrationConfig from './types/ServiceConfiguration';

const {
enabled = false,
activity_logging = false,
click_to_dial = false,
copilot_notes = false,
hide_crm_container = false,
prevent_popout_during_call = false,
screen_pop = false,
show_panel_automatically = false,
utility_bar_status = false,
} = (getFeatureFlags()?.features?.salesforce_integration as SalesforceIntegrationConfig) || {};

export const isFeatureEnabled = () => {
return enabled;
};

export const isActivityLoggingEnabled = () => {
return activity_logging;
};

export const isClickToDialEnabled = () => {
return click_to_dial;
};

export const isCopilotNotesEnabled = () => {
return copilot_notes;
};

export const isHideCrmContainerEnabled = () => {
return hide_crm_container;
};

export const isPreventPopoutEnabled = () => {
return prevent_popout_during_call;
};

export const isScreenPopEnabled = () => {
return screen_pop;
};

export const isShowPanelAutomaticallyEnabled = () => {
return show_panel_automatically;
};

export const isUtilityBarStatusEnabled = () => {
return utility_bar_status;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Actions, ITask } from '@twilio/flex-ui';
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Flex } from '@twilio-paste/core/flex';
import { Select, Option } from '@twilio-paste/core/select';

import AppState from '../../../types/manager/AppState';
import { reduxNamespace } from '../../../utils/state';
import { SalesforceIntegrationState } from '../flex-hooks/states';
import TaskRouterService from '../../../utils/serverless/TaskRouter/TaskRouterService';
import logger from '../../../utils/logger';

interface AssociateRecordDropdownProps {
task?: ITask;
}

const AssociateRecordDropdown = ({ task }: AssociateRecordDropdownProps) => {
const [isSaving, setIsSaving] = useState(false);

const { screenPopSearchResults } = useSelector(
(state: AppState) => state[reduxNamespace].salesforceIntegration as SalesforceIntegrationState,
);

if (!task || !screenPopSearchResults[task.sid]) {
return <></>;
}

const recordSelected = async (e: any) => {
const sfdcObjectId = e.target.value;

if (sfdcObjectId === 'placeholder') {
return;
}

const record = screenPopSearchResults[task.sid].find((item) => item.id === sfdcObjectId);

if (!record) {
return;
}

const alreadyBlocked = Actions.findBlockedActions('CompleteTask', { task });
const shouldBlock = Object.keys(alreadyBlocked).length < 1;

if (shouldBlock) {
Actions.blockAction('CompleteTask', { task });
}
try {
setIsSaving(true);
await TaskRouterService.updateTaskAttributes(task.taskSid, { sfdcObjectId, sfdcObjectType: record.type });
} catch (error: any) {
logger.error('[salesforce-integration] Error updating task attributes with record', error);
} finally {
setIsSaving(false);
}
if (shouldBlock) {
Actions.unblockAction('CompleteTask', { task });
}
};

return (
<Flex marginX="space50" marginY="space20">
<Select
id="associate-record-select"
value={task.attributes.sfdcObjectId ?? 'placeholder'}
onChange={recordSelected}
disabled={isSaving}
>
<Option disabled value="placeholder">
Select a record to associate...
</Option>
{screenPopSearchResults[task.sid].map((result) => (
<Option value={result.id}>
{result.name} - {result.type}
</Option>
))}
</Select>
</Flex>
);
};

export default AssociateRecordDropdown;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useFlexSelector } from '@twilio/flex-ui';
import { useEffect } from 'react';

import AppState from '../../../types/manager/AppState';
import { updateUtilityBar } from '../utils/UtilityBarHelper';

const StateListener = () => {
const { activity, tasks } = useFlexSelector((state: AppState) => state.flex.worker);

useEffect(() => {
updateUtilityBar(activity, tasks);
}, [activity, tasks]);

return null;
};

export default StateListener;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as Flex from '@twilio/flex-ui';

import { FlexActionEvent, FlexAction } from '../../../../types/feature-loader';
import { screenPop } from '../../utils/ScreenPop';
import { getOpenCti } from '../../utils/SfdcHelper';
import logger from '../../../../utils/logger';
import { isScreenPopEnabled } from '../../config';

export const actionEvent = FlexActionEvent.after;
export const actionName = FlexAction.AcceptTask;
export const actionHook = function screenPopAfterAccept(flex: typeof Flex) {
flex.Actions.addListener(`${actionEvent}${actionName}`, async (payload) => {
if (!isScreenPopEnabled() || !getOpenCti()) {
return;
}

let task;

if (payload.task) {
task = payload.task;
} else if (payload.sid) {
task = flex.TaskHelper.getTaskByTaskSid(payload.sid);
}

if (!task) {
return;
}

try {
screenPop(task);
} catch (error: any) {
logger.error('[salesforce-integration] Error calling Open CTI screenPop', error);
}
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Flex from '@twilio/flex-ui';

import { FlexActionEvent, FlexAction } from '../../../../types/feature-loader';
import { SalesforceIntegrationNotification } from '../notifications';

export const actionEvent = FlexActionEvent.after;
export const actionName = FlexAction.SetActivity;
export const actionHook = function clearNotificationAfterSetActivity(flex: typeof Flex) {
flex.Actions.addListener(`${actionEvent}${actionName}`, async () => {
flex.Notifications.dismissNotificationById(SalesforceIntegrationNotification.UnableToCallOffline);
});
};
Loading