Skip to content

Commit ddad8bf

Browse files
authored
Merge pull request #15 from Spinoco/feature/blacklist-domains
Introduced configuration rule concept and implemented way of disablin…
2 parents d42440f + 646d824 commit ddad8bf

5 files changed

Lines changed: 116 additions & 22 deletions

File tree

readme/configuration.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,32 @@ key: `features`
257257
| sendBoxInputBorder | boolean | | 1px solid border around input box with `borderColor` |
258258
| embedded | boolean | | Allows the webchat plugin to be embedded directly into a webview. Embeded plugin does not have header and may not be opened/closed.|
259259

260+
### Rules that allow to customize behaviour of the plugin based on certain criteria.
261+
262+
key `rules`
263+
264+
This is defined as array of the following objects. First rule matching applies others are ignored
265+
266+
267+
| Property | Type | Default | Description |
268+
|------------|-------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
269+
| **domain** | string, Array of string | | Required. Defines url of the domain this rule applies to. It is defined as string. If the string starts with "/" character, it is applied only to url path without hostName, otherwise it applies to hostname and path. Url is tested to start with given string, case ignored. multiple urls can be specified with array. |
270+
| disable | boolean | false | Disables webchat functionality, webchat will not initialize/render. |
271+
|
272+
Example of the rules configuration :
273+
274+
```json
275+
{
276+
"rules": [
277+
{
278+
"domain": "https://www.example.com",
279+
"disable": true
280+
}
281+
]
282+
}
283+
284+
```
285+
260286
## Configuration example
261287

262288
### Minimal

src/models/interfaces/configuration/configuration-interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AvatarInterface } from "./avatar-interface";
1010
import { DirectLineInterface } from "./direct-line-interface";
1111
import { LoaderInterface } from "./loader-interface";
1212
import { PopoverInterface } from "./popover/popover-interface";
13+
import { RuleInterface } from "./rule-interface";
1314

1415
export interface ConfigurationInterface {
1516
directLine: DirectLineInterface;
@@ -35,4 +36,5 @@ export interface ConfigurationInterface {
3536
timestampColor?: string;
3637
primaryFont?: string;
3738
variables?: Record<string, string | number>;
39+
rules?: [RuleInterface];
3840
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface RuleInterface {
2+
/** Domain for which this rule applies. Id the value starts with "/" it matched path in url only.
3+
* otherwise it matches host and path.
4+
* If the value is an array, the rule will apply to all domains in the array.
5+
* Comparions is case insensitive.
6+
* */
7+
domain: string | [string];
8+
9+
/** If true, disables chat functionality, chat plugin will not render */
10+
disable?: boolean;
11+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { RuleInterface } from "../interfaces/configuration/rule-interface";
2+
import { ConfigurationInterface } from "../interfaces/configuration/configuration-interface";
3+
4+
export class RuleService {
5+
private rule?: RuleInterface;
6+
7+
constructor(config: ConfigurationInterface) {
8+
this.rule = this.findRuleForCurrentDomain(config.rules ? config.rules : []);
9+
}
10+
11+
/**
12+
* Yields to true, if the chat may be displayed on the current domain.
13+
* If false, this means that the chat may not be displayed on the current domain.
14+
*/
15+
public mayDisplayForCurrentDomain(): boolean {
16+
return this.rule == null || this.rule.disable !== true;
17+
}
18+
19+
/**
20+
* Returns the rule for the current domain.
21+
*/
22+
private findRuleForCurrentDomain(rules: RuleInterface[]): RuleInterface | undefined {
23+
24+
return rules.find((rule) => {
25+
const domains = rule.domain instanceof Array ? rule.domain : [rule.domain];
26+
return domains.find(this.acceptDomain(window.location));
27+
});
28+
}
29+
30+
/**
31+
* Returns a function that checks if the domain matches the current domain.
32+
* @param currentPathName Path name where the chat is displayed.
33+
* @param currentHost Host where the chat is displayed.
34+
* @param currentPort Port where the chat is displayed.
35+
* @param currentProtocol Protocol where the chat is displayed.
36+
* @private
37+
*/
38+
private acceptDomain(
39+
location: Location
40+
): (domain: string) => boolean {
41+
return (domain: string) => {
42+
if (domain.startsWith("/")) {
43+
return location.pathname.toLowerCase().startsWith(domain.toLowerCase());
44+
} else {
45+
return location.href.toLowerCase().startsWith(domain.toLowerCase());
46+
}
47+
};
48+
}
49+
}

src/spinoco-webchat-plugin.tsx

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { config } from "./config/config";
1212
import { GlobalEventService } from "./models/services/global-event-service/global-event-service";
1313
import "./styles/app.scss";
1414
import { UrlNavigationService } from "./models/services/dom/url-navigation-service";
15+
import { RuleService } from "./models/services/rule-service";
1516

1617
declare global {
1718
interface Window {
@@ -30,29 +31,34 @@ const createWithConfigUrl = (url: string) => {
3031
fetch(`${url}`)
3132
.then((response) => response.json())
3233
.then((configuration: ConfigurationInterface) => {
33-
const directLineSecret = configuration.directLine.secret;
34-
if (!directLineSecret) {
35-
configuration.directLine.useMockbot = true;
36-
}
37-
const chatStorage = ChatStorage.getInstance();
38-
const conversationService = new ConversationService(chatStorage, configuration.directLine);
34+
const ruleSrevice = new RuleService(configuration);
35+
if (ruleSrevice.mayDisplayForCurrentDomain()) {
36+
const directLineSecret = configuration.directLine.secret;
37+
if (!directLineSecret) {
38+
configuration.directLine.useMockbot = true;
39+
}
40+
const chatStorage = ChatStorage.getInstance();
41+
const conversationService = new ConversationService(chatStorage, configuration.directLine);
3942

40-
ReactDOM.createRoot(chatDomService.wrapperElement).render(
41-
<React.StrictMode>
42-
<App
43-
chatStorage={chatStorage}
44-
storeService={storeService}
45-
localeService={localeService}
46-
conversationService={conversationService}
47-
customer={chatDomService.getCustomerDto()}
48-
bot={chatDomService.getBotDto()}
49-
popover={chatDomService.getPopoverDto()}
50-
configuration={configuration}
51-
globalEventService={globalEventService}
52-
urlNavigationService={urlNavigationService}
53-
/>
54-
</React.StrictMode>,
55-
);
43+
ReactDOM.createRoot(chatDomService.wrapperElement).render(
44+
<React.StrictMode>
45+
<App
46+
chatStorage={chatStorage}
47+
storeService={storeService}
48+
localeService={localeService}
49+
conversationService={conversationService}
50+
customer={chatDomService.getCustomerDto()}
51+
bot={chatDomService.getBotDto()}
52+
popover={chatDomService.getPopoverDto()}
53+
configuration={configuration}
54+
globalEventService={globalEventService}
55+
urlNavigationService={urlNavigationService}
56+
/>
57+
</React.StrictMode>,
58+
);
59+
} else {
60+
console.warn("Chat is disabled for this domain.");
61+
}
5662
})
5763
.catch((e) => {
5864
throw new FailedToLoadConfigurationError(e.message);

0 commit comments

Comments
 (0)