Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
3 changes: 3 additions & 0 deletions src/app/components/chat/chat.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Salesforce chat widget styling
// The chat widget is injected by Salesforce and appears as a fixed overlay
// Add custom styling overrides here if needed
154 changes: 154 additions & 0 deletions src/app/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from 'react';
import useUserContext from '~/contexts/user';
import './chat.scss';

declare global {
interface Window {
embeddedservice_bootstrap?: {
settings: {
language: string;
};
init: (
orgId: string,
deploymentName: string,
baseUrl: string,
options: {scrt2URL: string}
) => void;
prechatAPI?: {
setHiddenPrechatFields: (fields: Record<string, string>) => void;
};
};
__salesforceChatInitialized?: boolean;
}
}

const SALESFORCE_CONFIG = {
orgId: '00DU0000000Kwch',
deploymentName: 'Web_Messaging_Deployment',
baseUrl: 'https://openstax.my.site.com/ESWWebMessagingDeployme1716235390398',
scrt2URL: 'https://openstax.my.salesforce-scrt.com',
bootstrapScript: 'https://openstax.my.site.com/ESWWebMessagingDeployme1716235390398/assets/js/bootstrap.min.js'
};

function initEmbeddedMessaging() {
try {
if (window.embeddedservice_bootstrap) {
window.embeddedservice_bootstrap.settings.language = 'en_US';
window.embeddedservice_bootstrap.init(
SALESFORCE_CONFIG.orgId,
SALESFORCE_CONFIG.deploymentName,
SALESFORCE_CONFIG.baseUrl,
{scrt2URL: SALESFORCE_CONFIG.scrt2URL}
);
}
} catch (err) {
console.error('Error initializing Salesforce chat:', err);
}
}

// eslint-disable-next-line complexity
export default function Chat() {
const userContext = useUserContext();
const [scriptLoaded, setScriptLoaded] = React.useState(false);

Comment thread
RoyEJohnson marked this conversation as resolved.
Comment thread
RoyEJohnson marked this conversation as resolved.
// Derive stable user primitives
const userModel = userContext?.userModel;
const uuid = userModel?.uuid;
const firstName = userModel?.first_name;
const lastName = userModel?.last_name;
const email = userModel?.email;
const school = userModel?.accountsModel?.school_name;
Comment thread
RoyEJohnson marked this conversation as resolved.
Outdated

// Load Salesforce script once
React.useEffect(() => {
const script = document.createElement('script');

Comment thread
RoyEJohnson marked this conversation as resolved.
script.src = SALESFORCE_CONFIG.bootstrapScript;
script.type = 'text/javascript';
Comment thread
RoyEJohnson marked this conversation as resolved.
Outdated
script.async = true;

script.onload = () => {
setScriptLoaded(true);
};

script.onerror = () => {
console.error('Failed to load Salesforce chat script');
};

document.body.appendChild(script);

return () => {
if (document.body.contains(script)) {
document.body.removeChild(script);
}
Comment thread
RoyEJohnson marked this conversation as resolved.
// Hide the chat widget when component unmounts
// The Salesforce widget injects elements with these selectors
const chatElement = document.getElementById('embedded-messaging');

if (chatElement) {
chatElement.style.display = 'none';
}
Comment thread
RoyEJohnson marked this conversation as resolved.
// Note: Don't delete window.embeddedservice_bootstrap or __salesforceChatInitialized
// to maintain conversation state across component remounts
};
Comment thread
RoyEJohnson marked this conversation as resolved.
}, []);

// Show chat widget when component mounts (if it was previously hidden)
React.useEffect(() => {
if (scriptLoaded && window.__salesforceChatInitialized) {
const chatElement = document.getElementById('embedded-messaging');

if (chatElement) {
chatElement.style.removeProperty('display');
}
}
}, [scriptLoaded]);

// Initialize chat widget once (on first mount or after refresh)
React.useEffect(() => {
if (!scriptLoaded || !window.embeddedservice_bootstrap) {
return;
}

// Only initialize if not already initialized (persists across component remounts)
if (!window.__salesforceChatInitialized) {
initEmbeddedMessaging();
window.__salesforceChatInitialized = true;
}
Comment thread
RoyEJohnson marked this conversation as resolved.
}, [scriptLoaded]);

// Update pre-chat fields whenever user information changes
// This allows fields to update when a user logs in after chat is initialized
// eslint-disable-next-line complexity
React.useEffect(() => {
if (!scriptLoaded || !window.embeddedservice_bootstrap?.prechatAPI) {
return;
}
Comment thread
RoyEJohnson marked this conversation as resolved.

// Set sProduct for all users (logged in or not)
const hiddenFields: Record<string, string> = {
sProduct: 'Website'
};

// Add user information if available
if (uuid) {
hiddenFields.OpenStax_UUID__c = uuid; // eslint-disable-line camelcase
}
if (firstName) {
hiddenFields.FirstName = firstName;
}
if (lastName) {
hiddenFields.LastName = lastName;
}
if (email) {
hiddenFields.Email = email;
}
if (school) {
hiddenFields.School = school;
}

window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields(hiddenFields);
}, [scriptLoaded, uuid, firstName, lastName, email, school]);

return null;
}
40 changes: 40 additions & 0 deletions src/app/components/shell/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
generateFooterPageRoutes
} from './router-helpers/page-routes';
import {NonPortalRouteWrapper} from './router-helpers/non-portal-route-wrapper';
import useSharedDataContext from '~/contexts/shared-data';
import useUserContext from '~/contexts/user';
import Chat from '~/components/chat/chat';
import './skip-to-content.scss';

function doSkipToContent(event: React.MouseEvent) {
Expand Down Expand Up @@ -77,6 +80,42 @@ export default function Router() {

function MainRoutes() {
const {Layout} = useLayoutContext();
const {pathname} = useLocation();
const {flags} = useSharedDataContext();
const userContext = useUserContext();

// Determine if chat should be shown based on current route and feature flags
// eslint-disable-next-line complexity
const showChat = React.useMemo(() => {
if (!flags) {
return false;
}

// Check if user is logged in
const isLoggedIn = Boolean(userContext?.userModel?.uuid);

// If chat_logged_in is enabled and user is logged in, show on any page
if (flags.chat_logged_in && isLoggedIn) {
return true;
}
Comment thread
RoyEJohnson marked this conversation as resolved.
Outdated

// Check if we're on a book details page
if (pathname.startsWith('/details/') && flags.chat_book_details) {
return true;
}

// Check if we're on a subjects page
if (pathname.startsWith('/subjects') && flags.chat_subjects) {
return true;
}

// Check if we're on the contact page
if (pathname.startsWith('/contact') && flags.chat_contact) {
return true;
}

return false;
}, [pathname, flags, userContext]);

return (
<Layout>
Expand All @@ -91,6 +130,7 @@ function MainRoutes() {
<Route path="/details/*" element={<NonPortalRouteWrapper><DetailsRoutes /></NonPortalRouteWrapper>} />
<Route path="/:dir/*" element={<OtherPageRoutes />} />
</Routes>
{showChat && <Chat />}
</Layout>
);
}
9 changes: 8 additions & 1 deletion src/app/contexts/shared-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import buildContext from '~/components/jsx-helpers/build-context';
import cmsFetch from '~/helpers/cms-fetch';
import {usePromise} from '~/helpers/use-data';

type FlagName = 'myox_pardot' | 'my_openstax' | 'new_subjects';
type FlagName =
| 'myox_pardot'
| 'my_openstax'
| 'new_subjects'
| 'chat_book_details'
| 'chat_subjects'
| 'chat_contact'
| 'chat_logged_in';
Comment thread
RoyEJohnson marked this conversation as resolved.
Outdated

type Flag = {
name: FlagName;
Expand Down
Loading
Loading