Skip to content
Merged
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
10 changes: 10 additions & 0 deletions packages/template-retail-react-app/app/components/_app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@salesforce/commerce-sdk-react'
import logger from '@salesforce/retail-react-app/app/utils/logger-instance'
import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'

// Chakra
import {
Expand Down Expand Up @@ -80,6 +81,7 @@ import {

import Seo from '@salesforce/retail-react-app/app/components/seo'
import {Helmet} from 'react-helmet'
import ShopperAgent from '../shopper-agent/index'

const PlaceholderComponent = () => (
<Center p="2">
Expand Down Expand Up @@ -218,6 +220,7 @@ const App = (props) => {
// customer.
const {data: customer} = useCurrentCustomer()
const {data: basket} = useCurrentBasket()
const config = getConfig()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the linting doesn't like semi colons


const updateBasket = useShopperBasketsMutation('updateBasket')
const updateCustomerForBasket = useShopperBasketsMutation('updateCustomerForBasket')
Expand Down Expand Up @@ -373,6 +376,13 @@ const App = (props) => {
<link rel="alternate" hrefLang="x-default" href={`${appOrigin}/`} />
</Seo>

<ShopperAgent
commerceAgent={config.app.commerceAgent}
domainUrl={`${appOrigin}${buildUrl(location.pathname)}`}
locale={locale?.id}
basketId={basket?.id}
/>

<ScrollToTop />

<Box id="app" display="flex" flexDirection="column" flex={1}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import {useEffect, useState} from 'react'
import useScript from '@salesforce/retail-react-app/app/hooks/use-script'
import {useUsid} from '@salesforce/commerce-sdk-react'

const onClient = typeof window !== 'undefined'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the best practice is here. Admittedly this was copy and pasted from another spot.


// Function to initialize embedded messaging
const initEmbeddedMessaging = (
orgId,
embeddedServiceDeploymentName,
embeddedServiceDeploymentUrl,
scrt2Url
) => {
try {
if (
onClient &&
window.embeddedservice_bootstrap &&
window.embeddedservice_bootstrap.settings
) {
window.embeddedservice_bootstrap.settings.language = 'en_US'
window.embeddedservice_bootstrap.init(
orgId,
embeddedServiceDeploymentName,
embeddedServiceDeploymentUrl,
{
scrt2URL: scrt2Url
}
)
}
} catch (err) {
console.error('Error initializing Embedded Messaging: ', err)
}
}

function useMiaw(
scriptLoadStatus,
orgId,
embeddedServiceDeploymentName,
embeddedServiceDeploymentUrl,
scrt2Url
) {
const [isMiawInitialized, setIsMiawInitialized] = useState(false)

useEffect(() => {
if (scriptLoadStatus.loaded && !scriptLoadStatus.error) {
initEmbeddedMessaging(
orgId,
embeddedServiceDeploymentName,
embeddedServiceDeploymentUrl,
scrt2Url
)
setIsMiawInitialized(true)
}
}, [scriptLoadStatus])

return isMiawInitialized
}

/**
* ShopperAgent component that initializes and manages the embedded messaging service
* @param {Object} props - Component props
* @param {string} props.commerceAgent - JSON stringified commerce agent settings
* @param {string} props.domainUrl - The domain URL for the embedded messaging script
* @param {string} props.basketId - The basket ID for the embedded messaging script
* @param {string} props.locale - The locale for the embedded messaging script
* @param {function} props.onAgentConversationOpened - The callback function for when the conversation is opened
* @param {function} props.onAgentConversationClosed - The callback function for when the conversation is closed
* @returns {JSX.Element} The ShopperAgent component
*/
const ShopperAgent = ({
commerceAgent,
domainUrl,
basketId,
locale,
onAgentConversationOpened,
onAgentConversationClosed
}) => {
const { enabled, embeddedServiceName, embeddedServiceEndpoint, scriptSourceUrl, scrt2Url, salesforceOrgId, siteId } = JSON.parse(commerceAgent)
if (!onClient || !enabled) {
return null
}

const {usid} = useUsid();

useEffect(() => {
window.addEventListener('onEmbeddedMessagingReady', (e) => {
window.embeddedservice_bootstrap.prechatAPI.setHiddenPrechatFields({
DomainURL: domainUrl,
SiteId: siteId,
BasketId: basketId,
Locale: locale,
OrganizationId: salesforceOrgId,
UsId: usid

})
})

window.addEventListener('onEmbeddedMessagingConversationClosed', (e) => {
console.error('Error initializing Embedded Messaging: ', e)
})

window.addEventListener('onEmbeddedMessagingConversationOpened', (e) => {
console.log('Conversation opened', e)
})

window.addEventListener('onEmbeddedMessagingConversationEnded', (e) => {
console.log('Conversation ended', e)
})


}, [commerceAgent])

// Load the embedded messaging script
const scriptLoadStatus = useScript(scriptSourceUrl)

// Initialize the embedded messaging service
useMiaw(
scriptLoadStatus,
salesforceOrgId,
embeddedServiceName,
embeddedServiceEndpoint,
scrt2Url
)

// The component doesn't render anything visible
// It's just a wrapper for the embedded messaging service
return null
}

export default ShopperAgent
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import React from 'react'
import {render, screen, act} from '@testing-library/react'
import ShopperAgent from '@salesforce/retail-react-app/app/components/shopper-agent/index'
import useScript from '@salesforce/retail-react-app/app/hooks/use-script'
// Mock the embeddedservice_bootstrap object
const mockEmbeddedService = {
init: jest.fn(),
settings: jest.fn(),
prechatAPI: {
setHiddenPrechatFields: jest.fn()
}
}

jest.mock('../../hooks/use-script', () => jest.fn().mockReturnValue({loaded: false, error: false}))
jest.mock('@salesforce/commerce-sdk-react', () => {
const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
return {
...originalModule,
useUsid: () => ({ usid: 'test-usid' })
}
})

const commerceAgentSettings = {
enabled: "true",
embeddedServiceName: "MIAW_Guided_Shopper_production",
embeddedServiceEndpoint: "https://myorg.salesforce.com/ESWMIAWGuidedShopper",
scriptSourceUrl: "https://myorg.salesforce.com/ESWMIAWGuidedShopper/assets/js/bootstrap.min.js",
scrt2Url: "https://myorg.salesforce.com-scrt.com",
salesforceOrgId: "00DSB00000MJ7YH",
siteId: "RefArchGlobal",
}

const commerceAgentSettingsString = JSON.stringify(commerceAgentSettings);

describe('ShopperAgent Component', () => {
const defaultProps = {
commerceAgent: commerceAgentSettingsString,
domainUrl: 'https://myorg.salesforce.com',
basketId: undefined, // TODO: Add basketId
locale: 'en-US'
}

beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks()

// Mock the window.embeddedservice_bootstrap object
global.window.embeddedservice_bootstrap = mockEmbeddedService

useScript.mockReturnValue({loaded: false, error: false})

// Clear any existing scripts
const scripts = document.querySelectorAll('script[data-status]')
scripts.forEach((script) => script.remove())
})

afterEach(() => {
// Clean up the window.embeddedservice_bootstrap mock
delete global.window.embeddedservice_bootstrap
})

test('should render nothing when enableMiaw is false', () => {
const props = {...defaultProps, enableMiaw: false}
const {container} = render(<ShopperAgent {...props} />)

expect(container.firstChild).toBeNull()
})

test('should not render anything when commerceAgenticEsdScriptSourceUrl is not provided', () => {
const props = {...defaultProps, scriptUrl: null}
const {container} = render(<ShopperAgent {...props} />)
expect(container.firstChild).toBeNull()
})

test('should not render anything when embeddedservice_bootstrap is not available', () => {
// Temporarily remove the mock for this test
const originalEmbeddedService = global.window.embeddedservice_bootstrap
delete global.window.embeddedservice_bootstrap
useScript.mockReturnValue({loaded: true, error: false})

render(<ShopperAgent {...defaultProps} />)

expect(mockEmbeddedService.init).not.toHaveBeenCalled()

// Restore the mock
global.window.embeddedservice_bootstrap = originalEmbeddedService
})

test('should initialize embedded service when all required props are provided', () => {
useScript.mockReturnValue({loaded: true, error: false})
render(<ShopperAgent {...defaultProps} />)

// Verify embedded service initialization
expect(mockEmbeddedService.init).toHaveBeenCalledWith(
commerceAgentSettings.salesforceOrgId,
commerceAgentSettings.embeddedServiceName,
commerceAgentSettings.embeddedServiceEndpoint,
{
scrt2URL: commerceAgentSettings.scrt2Url
}
)
})

test('should handle initialization error from useMiaw hook', () => {
// Mock useMiaw to return an error
const errorMessage = 'Initialization failed'
useScript.mockReturnValue({loaded: true, error: true})
mockEmbeddedService.init.mockImplementation(() => {
throw new Error(errorMessage)
})

const {container} = render(<ShopperAgent {...defaultProps} />)

// Component should not render anything when there's an error
expect(container.firstChild).toBeNull()
})

test('should not reinitialize embedded service when already initialized', () => {
// First render
const scriptLoadStatus = {loaded: true, error: false}
useScript.mockReturnValue(scriptLoadStatus)
const {rerender} = render(<ShopperAgent {...defaultProps} />)

expect(mockEmbeddedService.init).toHaveBeenCalled()

// Reset mock call counts
jest.clearAllMocks()

useScript.mockReturnValue(scriptLoadStatus)

// Re-render with same props
rerender(<ShopperAgent {...defaultProps} />)

// Should not call init or createComponent again
expect(mockEmbeddedService.init).not.toHaveBeenCalled()
})

test('should call the preChatAPI with the correct parameters', async () => {
useScript.mockReturnValue({loaded: true, error: false})
render(<ShopperAgent {...defaultProps} />)

await act(async () => {
window.dispatchEvent(new Event('onEmbeddedMessagingReady'));
});

// Verify embedded service initialization
expect(mockEmbeddedService.prechatAPI.setHiddenPrechatFields).toHaveBeenCalledWith(
{
BasketId: undefined,
DomainURL: defaultProps.domainUrl,
Locale: defaultProps.locale,
OrganizationId: commerceAgentSettings.salesforceOrgId,
SiteId: commerceAgentSettings.siteId,
UsId: 'test-usid',
}
)
})
})
49 changes: 49 additions & 0 deletions packages/template-retail-react-app/app/hooks/use-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import {useEffect, useState} from 'react'

/**
* Custom hook to handle script loading
* @param {string} src - The source URL for the script
* @returns {Object} The script load status
*/
const useScript = (src) => {
const [scriptLoadStatus, setScriptLoadStatus] = useState({loaded: false, error: false})
// Effect to load and initialize the script
useEffect(() => {
if (!src) {
return
}

// Check if script already exists
const scriptAlreadyOnPage = document.querySelector(`script[src="${src}"]`)

if (!scriptAlreadyOnPage) {
const script = document.createElement('script')
script.src = src
script.async = true
script.setAttribute('data-status', 'loading')
document.body.appendChild(script)

const onScriptLoad = (event) => {
const loadStatus = event.type === 'load' ? 'ready' : 'error'
setScriptLoadStatus({
loaded: loadStatus === 'ready',
error: loadStatus === 'error'
})
}

script.addEventListener('load', onScriptLoad)
script.addEventListener('error', onScriptLoad)
}
}, [src])

return scriptLoadStatus
}

export default useScript
Loading
Loading