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 packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const accountCapabilities = [
'Object Storage',
'Placement Group',
'SMTP Enabled',
'Support Live Chat',
'Support Ticket Severity',
'Vlans',
'VPCs',
Expand Down
23 changes: 22 additions & 1 deletion packages/api-v4/src/support/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
createSupportTicketSchema,
} from '@linode/validation/lib/support.schema';

import { API_ROOT } from '../constants';
import { API_ROOT, BETA_API_ROOT } from '../constants';
import Request, {
setData,
setMethod,
Expand Down Expand Up @@ -152,3 +152,24 @@ export const uploadAttachment = (ticketId: number, formData: FormData) =>
setMethod('POST'),
setData(formData),
);

/**
* Recive Support liveChat Token
* @param TokenID { String } the ID of the token to be retrieved
*/

export const getLiveChatToken = (params?: Params, filter?: Filter) =>
Request<{ token: string }>(
setURL(`${BETA_API_ROOT}/support/chat_token`),
setMethod('GET'),
setParams(params),
setXFilter(filter),
);

export const getLiveChatStatus = (params?: Params, filter?: Filter) =>
Request<{ live_chat_available: string }>(
setURL(`${BETA_API_ROOT}/support/live_chat_available/`),
setMethod('GET'),
setParams(params),
setXFilter(filter),
);
7 changes: 7 additions & 0 deletions packages/manager/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ REACT_APP_APP_ROOT='http://localhost:3000'
# Pendo:
# REACT_APP_PENDO_API_KEY=

# Embedded live chat URLs:
# REACT_APP_CHAT_DEPLOYMENT_URL=
# REACT_APP_CHAT_SCRT2_URL=
# REACT_APP_CHAT_BOOTSTRAP_JS_URL=
# REACT_APP_CHAT_ORG_ID=
# REACT_APP_CHAT_DEPLOYMENT_NAME=

# Linode Docs search with Algolia:
# REACT_APP_ALGOLIA_APPLICATION_ID=
# REACT_APP_ALGOLIA_SEARCH_KEY=
Expand Down
254 changes: 243 additions & 11 deletions packages/manager/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,245 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Akamai Cloud Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Akamai Cloud Manager</title>
<style>
.embeddedMessagingFrame {
color-scheme: light !important;
}
</style>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script type="text/javascript">
let hasLiveChatInitialized = false;
let embeddedMessagingScriptPromise;
let chatEventListenersAttached = false;
let hasTriggeredChatCloseRefresh = false;

const embeddedMessagingConfig = {
bootstrapJsUrl: '%REACT_APP_CHAT_BOOTSTRAP_JS_URL%',
deploymentUrl: '%REACT_APP_CHAT_DEPLOYMENT_URL%',
scrt2Url: '%REACT_APP_CHAT_SCRT2_URL%',
orgId: '%REACT_APP_CHAT_ORG_ID%',
deploymentName: '%REACT_APP_CHAT_DEPLOYMENT_NAME%',
};

function loadEmbeddedMessagingScript() {
if (window.embeddedservice_bootstrap) {
return Promise.resolve();
}

if (embeddedMessagingScriptPromise) {
return embeddedMessagingScriptPromise;
}

embeddedMessagingScriptPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = embeddedMessagingConfig.bootstrapJsUrl;
script.onload = () => resolve();
script.onerror = () => {
embeddedMessagingScriptPromise = undefined;
reject(new Error('Failed to load Embedded Messaging script.'));
};
document.body.appendChild(script);
});

return embeddedMessagingScriptPromise;
}

function hideEmbeddedMessagingContainer() {
const selectors = [
'#embeddedmessaging-container',
'#embeddedMessaging-container',
'[id*="embeddedmessaging-container"]',
'[id*="embeddedMessaging-container"]',
'[id*="embeddedMessaging"]',
'[class*="embeddedMessaging"]',
'iframe[id*="embeddedMessaging"]',
'iframe[title*="chat" i]',
'button[id*="embeddedMessaging"]',
'button[title*="chat" i]',
];

selectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((element) => {
element.style.setProperty('display', 'none', 'important');
element.style.setProperty('visibility', 'hidden', 'important');
element.style.setProperty('opacity', '0', 'important');
element.style.setProperty('pointer-events', 'none', 'important');
element.setAttribute('aria-hidden', 'true');
});
});

try {
const utilAPI = window.embeddedservice_bootstrap?.utilAPI;
utilAPI?.minimizeChat?.();
utilAPI?.hideChatButton?.();
} catch { }
}

function handleChatClosed() {
hideEmbeddedMessagingContainer();
hasLiveChatInitialized = false;
window.sessionStorage.removeItem('EnableLiveChat');

// Reset stale chat state by reloading once after a close event.
if (!hasTriggeredChatCloseRefresh) {
hasTriggeredChatCloseRefresh = true;
window.location.reload();
}
}

function attachEmbeddedMessagingLifecycleListeners() {
if (chatEventListenersAttached) return;
chatEventListenersAttached = true;

const closeEvents = ['onEmbeddedMessagingWindowClosed'];

closeEvents.forEach((eventName) => {
window.addEventListener(eventName, () => {
const iframe =
document.querySelector('iframe[id*="embeddedMessaging"]') ||
document.querySelector('iframe[title*="chat" i]');

// If the iframe is gone or hidden, treat it as closed.
setTimeout(() => {
const iframeStillVisible =
iframe &&
document.body.contains(iframe) &&
window.getComputedStyle(iframe).display !== 'none' &&
window.getComputedStyle(iframe).visibility !== 'hidden';

if (!iframeStillVisible) {
handleChatClosed();
}
}, 300);
});
});

window.addEventListener('message', (event) => {
const payload =
typeof event.data === 'string'
? (() => {
try {
return JSON.parse(event.data);
} catch {
return { action: event.data };
}
})()
: event.data ?? {};

const action = payload?.action ?? payload?.type ?? payload?.event;

if (
action === 'prechatLoaded' ||
action === 'prechat:loaded' ||
action === 'embeddedMessaging:prechatLoaded'
) {
const token = window.sessionStorage.getItem('LiveChatToken');
const subject = window.sessionStorage.getItem('LiveChatSubject');

const dataMap = {
JWE_Token: token ?? '',
Title: subject ?? '',
};

if (event.source && event.origin) {
event.source.postMessage(
{ action: 'hiddenParameters', data: dataMap },
event.origin
);
}

window.sessionStorage.removeItem('LiveChatToken');
} else if (action === 'chatInitiationResult') {
const subject = window.sessionStorage.getItem('LiveChatSubject');
const description = window.sessionStorage.getItem('LiveChatDescription');

window.sessionStorage.removeItem('LiveChatSubject');
window.sessionStorage.removeItem('LiveChatDescription');

if (payload?.data?.statusCode !== 200) {
hasLiveChatInitialized = false;
hideEmbeddedMessagingContainer();
window.dispatchEvent(
new CustomEvent('manager:live-chat-failed', {
detail: {
description: description ?? '',
subject: subject ?? '',
errorType: payload?.data?.type,
errorDescription: payload?.data?.description,
},
})
);
}
} else if (action === 'onEmbeddedMessagingWindowClosed') {
handleChatClosed();
}
});
}

async function openLiveChatOnce() {
const enableLiveChat =
window.sessionStorage.getItem('EnableLiveChat') === 'true';

if (!enableLiveChat || hasLiveChatInitialized) {
return;
}

hasLiveChatInitialized = true;

const liveChatToken = window.sessionStorage.getItem('LiveChatToken');
if (!liveChatToken) {
hasLiveChatInitialized = false;
return;
}

try {
await loadEmbeddedMessagingScript();
} catch {
hasLiveChatInitialized = false;
return;
}

attachEmbeddedMessagingLifecycleListeners();
window.sessionStorage.removeItem('EnableLiveChat');

window.addEventListener(
'onEmbeddedMessagingButtonCreated',
() => {
try {
window.embeddedservice_bootstrap?.utilAPI?.launchChat?.();
} catch { }
},
{ once: true }
);

embeddedservice_bootstrap.settings.language = 'en_US';
embeddedservice_bootstrap.init(
embeddedMessagingConfig.orgId,
embeddedMessagingConfig.deploymentName,
embeddedMessagingConfig.deploymentUrl,
{
scrt2URL: embeddedMessagingConfig.scrt2Url,
}
);
}

function initEmbeddedMessaging() {
window.addEventListener('manager:enable-live-chat', openLiveChatOnce);
openLiveChatOnce();
}

initEmbeddedMessaging();
</script>
</body>

</html>
1 change: 1 addition & 0 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'limitsEvolution', label: 'Limits Evolution' },
{ flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' },
{ flag: 'linodeInterfaces', label: 'Linode Interfaces' },
{ flag: 'liveChat', label: 'Live Chat' },
{ flag: 'lkeEnterprise2', label: 'LKE-Enterprise' },
{ flag: 'marketplaceV2', label: 'MarketplaceV2' },
{ flag: 'networkLoadBalancer', label: 'Network Load Balancer' },
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/factories/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const accountFactory = Factory.Sync.makeFactory<Account>({
'Object Storage Endpoint Types',
'Object Storage',
'Placement Group',
'Support Live Chat',
'Vlans',
'Kubernetes Enterprise',
'VPC Dual Stack',
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export interface Flags {
linodeCreateBanner: LinodeCreateBanner;
linodeDiskEncryption: boolean;
linodeInterfaces: LinodeInterfacesFlag;
liveChat: boolean;
lkeEnterprise2: LkeEnterpriseFlag;
mainContentBanner: MainContentBanner;
marketplaceAppOverrides: MarketplaceAppOverride[];
Expand Down
Loading