@salesforce/retail-react-app 6.0.0 → 6.1.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,404 @@
1
+ /*
2
+ * Copyright (c) 2025, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+ import {expect} from '@jest/globals'
8
+
9
+ export const mockLoginViewPageEvent = {
10
+ events: [
11
+ expect.objectContaining({
12
+ guestId: 'guest-usid',
13
+ siteId: 'RefArch',
14
+ sessionId: expect.any(String),
15
+ deviceId: expect.any(String),
16
+ dateTime: expect.any(String),
17
+ customerId: 1234567890,
18
+ eventId: expect.any(String),
19
+ eventType: 'identity',
20
+ category: 'Profile',
21
+ isAnonymous: 0,
22
+ firstName: 'John',
23
+ lastName: 'Smith',
24
+ sourceUrl: '/login'
25
+ }),
26
+ expect.objectContaining({
27
+ guestId: 'guest-usid',
28
+ siteId: 'RefArch',
29
+ sessionId: expect.any(String),
30
+ deviceId: expect.any(String),
31
+ dateTime: expect.any(String),
32
+ customerId: 1234567890,
33
+ eventId: expect.any(String),
34
+ eventType: 'userEngagement',
35
+ category: 'Engagement',
36
+ interactionName: 'page-view',
37
+ sourceUrl: '/login'
38
+ })
39
+ ]
40
+ }
41
+
42
+ export const mockLoginViewPageEventDNT = {
43
+ events: [
44
+ expect.objectContaining({
45
+ guestId: '__DNT__',
46
+ siteId: 'RefArch',
47
+ sessionId: '__DNT__',
48
+ deviceId: '__DNT__',
49
+ dateTime: expect.any(String),
50
+ customerId: '__DNT__',
51
+ customerNo: '__DNT__',
52
+ eventId: expect.any(String),
53
+ eventType: 'userEngagement',
54
+ category: 'Engagement',
55
+ interactionName: 'page-view',
56
+ sourceUrl: '/login'
57
+ })
58
+ ]
59
+ }
60
+
61
+ export const mockViewProductEvent = {
62
+ events: [
63
+ expect.objectContaining({
64
+ guestId: 'guest-usid',
65
+ siteId: 'RefArch',
66
+ sessionId: expect.any(String),
67
+ deviceId: expect.any(String),
68
+ dateTime: expect.any(String),
69
+ customerId: 1234567890,
70
+ eventId: expect.any(String),
71
+ eventType: 'identity',
72
+ category: 'Profile',
73
+ isAnonymous: 0,
74
+ firstName: 'John',
75
+ lastName: 'Smith'
76
+ }),
77
+ expect.objectContaining({
78
+ guestId: 'guest-usid',
79
+ siteId: 'RefArch',
80
+ sessionId: expect.any(String),
81
+ deviceId: expect.any(String),
82
+ dateTime: expect.any(String),
83
+ customerId: 1234567890,
84
+ eventId: expect.any(String),
85
+ eventType: 'contactPointEmail',
86
+ category: 'Profile',
87
+ email: 'johnsmith@salesforce.com'
88
+ }),
89
+ expect.objectContaining({
90
+ guestId: 'guest-usid',
91
+ siteId: 'RefArch',
92
+ sessionId: expect.any(String),
93
+ deviceId: expect.any(String),
94
+ dateTime: expect.any(String),
95
+ customerId: 1234567890,
96
+ eventId: expect.any(String),
97
+ eventType: 'catalog',
98
+ category: 'Engagement',
99
+ id: '56736828M',
100
+ type: 'Product',
101
+ webStoreId: 'pwa',
102
+ interactionName: 'catalog-object-view-start'
103
+ })
104
+ ]
105
+ }
106
+
107
+ export const mockCategorySearchParams = {
108
+ limit: 25,
109
+ offset: 0,
110
+ sort: 'best-matches'
111
+ }
112
+
113
+ export const mockViewCategoryEvent = {
114
+ events: [
115
+ expect.objectContaining({
116
+ guestId: 'guest-usid',
117
+ siteId: 'RefArch',
118
+ sessionId: expect.any(String),
119
+ deviceId: expect.any(String),
120
+ dateTime: expect.any(String),
121
+ customerId: 1234567890,
122
+ eventId: expect.any(String),
123
+ eventType: 'identity',
124
+ category: 'Profile',
125
+ isAnonymous: 0,
126
+ firstName: 'John',
127
+ lastName: 'Smith'
128
+ }),
129
+ expect.objectContaining({
130
+ guestId: 'guest-usid',
131
+ siteId: 'RefArch',
132
+ sessionId: expect.any(String),
133
+ deviceId: expect.any(String),
134
+ dateTime: expect.any(String),
135
+ customerId: 1234567890,
136
+ eventId: expect.any(String),
137
+ eventType: 'catalog',
138
+ category: 'Engagement',
139
+ searchResultTitle: undefined,
140
+ searchResultPosition: 0,
141
+ searchResultPageNumber: 1,
142
+ id: '25752986M',
143
+ type: 'Product',
144
+ webStoreId: 'pwa',
145
+ categoryId: 'mens-accessories-ties',
146
+ interactionName: 'catalog-object-impression'
147
+ }),
148
+ expect.objectContaining({
149
+ guestId: 'guest-usid',
150
+ siteId: 'RefArch',
151
+ sessionId: expect.any(String),
152
+ deviceId: expect.any(String),
153
+ dateTime: expect.any(String),
154
+ customerId: 1234567890,
155
+ eventId: expect.any(String),
156
+ eventType: 'catalog',
157
+ category: 'Engagement',
158
+ searchResultTitle: undefined,
159
+ searchResultPosition: 0,
160
+ searchResultPageNumber: 1,
161
+ id: '25752235M',
162
+ type: 'Product',
163
+ webStoreId: 'pwa',
164
+ categoryId: 'mens-accessories-ties',
165
+ interactionName: 'catalog-object-impression'
166
+ }),
167
+ expect.objectContaining({
168
+ guestId: 'guest-usid',
169
+ siteId: 'RefArch',
170
+ sessionId: expect.any(String),
171
+ deviceId: expect.any(String),
172
+ dateTime: expect.any(String),
173
+ customerId: 1234567890,
174
+ eventId: expect.any(String),
175
+ eventType: 'catalog',
176
+ category: 'Engagement',
177
+ searchResultTitle: undefined,
178
+ searchResultPosition: 0,
179
+ searchResultPageNumber: 1,
180
+ id: '25752218M',
181
+ type: 'Product',
182
+ webStoreId: 'pwa',
183
+ categoryId: 'mens-accessories-ties',
184
+ interactionName: 'catalog-object-impression'
185
+ }),
186
+ expect.objectContaining({
187
+ guestId: 'guest-usid',
188
+ siteId: 'RefArch',
189
+ sessionId: expect.any(String),
190
+ deviceId: expect.any(String),
191
+ dateTime: expect.any(String),
192
+ customerId: 1234567890,
193
+ eventId: expect.any(String),
194
+ eventType: 'catalog',
195
+ category: 'Engagement',
196
+ searchResultTitle: undefined,
197
+ searchResultPosition: 0,
198
+ searchResultPageNumber: 1,
199
+ id: '25752981M',
200
+ type: 'Product',
201
+ webStoreId: 'pwa',
202
+ categoryId: 'mens-accessories-ties',
203
+ interactionName: 'catalog-object-impression'
204
+ })
205
+ ]
206
+ }
207
+
208
+ export const mockSearchParam = {
209
+ limit: 25,
210
+ offset: 0,
211
+ q: 'oxford glove',
212
+ sort: 'best-matches'
213
+ }
214
+
215
+ export const mockGloveSearchResult = {
216
+ limit: 1,
217
+ hits: [
218
+ {
219
+ currency: 'GBP',
220
+ hitType: 'master',
221
+ image: {
222
+ alt: "Men's Oxford Gloves, , large",
223
+ disBaseLink:
224
+ 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwb69853b8/images/large/TG250_206.jpg',
225
+ link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dwb69853b8/images/large/TG250_206.jpg',
226
+ title: "Men's Oxford Gloves, "
227
+ },
228
+ price: 63.99,
229
+ pricePerUnit: 63.99,
230
+ priceRanges: [
231
+ {
232
+ maxPrice: 63.99,
233
+ minPrice: 63.99,
234
+ pricebook: 'gbp-m-list-prices'
235
+ }
236
+ ],
237
+ productId: 'TG250M',
238
+ productName: "Men's Oxford Gloves",
239
+ productType: {
240
+ master: true
241
+ },
242
+ c_productUrl: 'https://pwa-kit.mobify-storefront.com/global/en-GB/product/TG250M'
243
+ }
244
+ ],
245
+ query: 'oxford glove',
246
+ refinements: [
247
+ {
248
+ attributeId: 'cgid',
249
+ label: 'Category',
250
+ values: [
251
+ {
252
+ hitCount: 1,
253
+ label: 'Mens',
254
+ value: 'mens',
255
+ values: [
256
+ {
257
+ hitCount: 1,
258
+ label: 'Accessories',
259
+ value: 'mens-accessories',
260
+ values: [
261
+ {
262
+ hitCount: 1,
263
+ label: 'Gloves',
264
+ value: 'mens-accessories-gloves'
265
+ }
266
+ ]
267
+ }
268
+ ]
269
+ }
270
+ ]
271
+ },
272
+ {
273
+ attributeId: 'c_refinementColor',
274
+ label: 'Colour',
275
+ values: [
276
+ {
277
+ hitCount: 0,
278
+ label: 'Beige',
279
+ presentationId: 'beige',
280
+ value: 'Beige'
281
+ },
282
+ {
283
+ hitCount: 0,
284
+ label: 'Black',
285
+ presentationId: 'black',
286
+ value: 'Black'
287
+ }
288
+ ]
289
+ },
290
+ {
291
+ attributeId: 'price',
292
+ label: 'Price',
293
+ values: [
294
+ {
295
+ hitCount: 1,
296
+ label: '£50 - £99.99',
297
+ value: '(50..100)'
298
+ }
299
+ ]
300
+ }
301
+ ],
302
+ selectedSortingOption: 'best-matches',
303
+ sortingOptions: [
304
+ {
305
+ id: 'best-matches',
306
+ label: 'Best Matches'
307
+ }
308
+ ],
309
+ offset: 0,
310
+ total: 1
311
+ }
312
+
313
+ export const mockViewSearchResultsEvent = {
314
+ events: [
315
+ expect.objectContaining({
316
+ guestId: 'guest-usid',
317
+ siteId: 'RefArch',
318
+ sessionId: expect.any(String),
319
+ deviceId: expect.any(String),
320
+ dateTime: expect.any(String),
321
+ customerId: 1234567890,
322
+ eventId: expect.any(String),
323
+ eventType: 'identity',
324
+ category: 'Profile',
325
+ isAnonymous: 0,
326
+ firstName: 'John',
327
+ lastName: 'Smith'
328
+ }),
329
+ expect.objectContaining({
330
+ guestId: 'guest-usid',
331
+ siteId: 'RefArch',
332
+ sessionId: expect.any(String),
333
+ deviceId: expect.any(String),
334
+ dateTime: expect.any(String),
335
+ customerId: 1234567890,
336
+ eventId: expect.any(String),
337
+ eventType: 'catalog',
338
+ category: 'Engagement',
339
+ searchResultTitle: 'oxford glove',
340
+ searchResultPosition: 0,
341
+ searchResultPageNumber: 1,
342
+ searchResultId: expect.any(String),
343
+ id: 'TG250M',
344
+ type: 'Product',
345
+ webStoreId: 'pwa',
346
+ interactionName: 'catalog-object-impression'
347
+ })
348
+ ]
349
+ }
350
+
351
+ export const mockRecommendationIds = [{id: '11111111'}, {id: '22222222'}]
352
+
353
+ export const mockViewRecommendationsEvent = {
354
+ events: [
355
+ expect.objectContaining({
356
+ guestId: 'guest-usid',
357
+ siteId: 'RefArch',
358
+ sessionId: expect.any(String),
359
+ deviceId: expect.any(String),
360
+ dateTime: expect.any(String),
361
+ customerId: 1234567890,
362
+ eventId: expect.any(String),
363
+ eventType: 'identity',
364
+ category: 'Profile',
365
+ isAnonymous: 0,
366
+ firstName: 'John',
367
+ lastName: 'Smith'
368
+ }),
369
+ expect.objectContaining({
370
+ guestId: 'guest-usid',
371
+ siteId: 'RefArch',
372
+ sessionId: expect.any(String),
373
+ deviceId: expect.any(String),
374
+ dateTime: expect.any(String),
375
+ customerId: 1234567890,
376
+ eventId: expect.any(String),
377
+ eventType: 'catalog',
378
+ category: 'Engagement',
379
+ id: '11111111',
380
+ type: 'Product',
381
+ webStoreId: 'pwa',
382
+ interactionName: 'catalog-object-impression',
383
+ personalizationId: 'testRecommender',
384
+ personalizationContextId: '883360544021M'
385
+ }),
386
+ expect.objectContaining({
387
+ guestId: 'guest-usid',
388
+ siteId: 'RefArch',
389
+ sessionId: expect.any(String),
390
+ deviceId: expect.any(String),
391
+ dateTime: expect.any(String),
392
+ customerId: 1234567890,
393
+ eventId: expect.any(String),
394
+ eventType: 'catalog',
395
+ category: 'Engagement',
396
+ id: '22222222',
397
+ type: 'Product',
398
+ webStoreId: 'pwa',
399
+ interactionName: 'catalog-object-impression',
400
+ personalizationId: 'testRecommender',
401
+ personalizationContextId: '883360544021M'
402
+ })
403
+ ]
404
+ }
@@ -41,6 +41,7 @@ import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation
41
41
  import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
42
42
  import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
43
43
  import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
44
+ import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
44
45
  import {useAuthHelper, AuthHelpers} from '@salesforce/commerce-sdk-react'
45
46
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
46
47
  import {isHydrated} from '@salesforce/retail-react-app/app/utils/utils'
@@ -94,11 +95,13 @@ const Account = () => {
94
95
  const [showLoading, setShowLoading] = useState(false)
95
96
 
96
97
  const einstein = useEinstein()
98
+ const dataCloud = useDataCloud()
97
99
 
98
100
  const {buildUrl} = useMultiSite()
99
101
  /**************** Einstein ****************/
100
102
  useEffect(() => {
101
103
  einstein.sendViewPage(location.pathname)
104
+ dataCloud.sendViewPage(location.pathname)
102
105
  }, [location])
103
106
 
104
107
  const onSignoutClick = async () => {
@@ -20,11 +20,12 @@ import {
20
20
  mockOrderProducts,
21
21
  mockPasswordUpdateFalure
22
22
  } from '@salesforce/retail-react-app/app/mocks/mock-data'
23
+ import {useCustomerType} from '@salesforce/commerce-sdk-react'
23
24
  import Account from '@salesforce/retail-react-app/app/pages/account/index'
24
25
  import Login from '@salesforce/retail-react-app/app/pages/login'
25
26
  import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
26
- import * as sdk from '@salesforce/commerce-sdk-react'
27
27
 
28
+ jest.setTimeout(60000)
28
29
  jest.mock('@salesforce/commerce-sdk-react', () => ({
29
30
  ...jest.requireActual('@salesforce/commerce-sdk-react'),
30
31
  useCustomerType: jest.fn()
@@ -73,7 +74,7 @@ beforeEach(() => {
73
74
  })
74
75
  afterEach(() => {
75
76
  jest.resetModules()
76
- localStorage.clear()
77
+ jest.restoreAllMocks()
77
78
  })
78
79
 
79
80
  const expectedBasePath = '/uk/en-GB'
@@ -86,50 +87,45 @@ describe('Test redirects', function () {
86
87
  )
87
88
  })
88
89
  test('Redirects to login page if the customer is not logged in', async () => {
89
- sdk.useCustomerType.mockReturnValue({isRegistered: false, isGuest: true})
90
- const Component = () => {
91
- return (
92
- <Switch>
93
- <Route
94
- path={createPathWithDefaults('/account')}
95
- render={(props) => <Account {...props} />}
96
- />
97
- </Switch>
98
- )
99
- }
100
- renderWithProviders(<Component />, {
90
+ useCustomerType.mockReturnValue({isRegistered: false, isGuest: true})
91
+ renderWithProviders(<MockedComponent />, {
101
92
  wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app, isGuest: true}
102
93
  })
103
94
  await waitFor(() => expect(window.location.pathname).toBe(`${expectedBasePath}/login`))
104
95
  })
105
96
  })
106
-
107
- test('Provides navigation for subpages', async () => {
108
- sdk.useCustomerType.mockReturnValue({isRegistered: true, isGuest: false})
109
- global.server.use(
110
- rest.get('*/products', (req, res, ctx) => {
111
- return res(ctx.delay(0), ctx.json(mockOrderProducts))
112
- }),
113
- rest.get('*/customers/:customerId/orders', (req, res, ctx) => {
114
- return res(ctx.delay(0), ctx.json(mockOrderHistory))
97
+ describe('Page Navigation', () => {
98
+ test('works for subpages', async () => {
99
+ useCustomerType.mockReturnValue({isRegistered: true, isGuest: false})
100
+ global.server.use(
101
+ rest.get('*/products', (req, res, ctx) => {
102
+ return res(ctx.delay(0), ctx.json(mockOrderProducts))
103
+ }),
104
+ rest.get('*/customers/:customerId/orders', (req, res, ctx) => {
105
+ return res(ctx.delay(0), ctx.json(mockOrderHistory))
106
+ })
107
+ )
108
+ const {user} = renderWithProviders(<MockedComponent />, {
109
+ wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
115
110
  })
116
- )
117
- const {user} = renderWithProviders(<MockedComponent />, {
118
- wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
119
- })
120
- expect(await screen.findByTestId('account-page')).toBeInTheDocument()
111
+ expect(await screen.findByTestId('account-page')).toBeInTheDocument()
121
112
 
122
- const nav = within(screen.getByTestId('account-detail-nav'))
123
- await user.click(nav.getByText('Addresses'))
124
- await waitFor(() =>
125
- expect(window.location.pathname).toBe(`${expectedBasePath}/account/addresses`)
126
- )
127
- await user.click(nav.getByText('Order History'))
128
- await waitFor(() => expect(window.location.pathname).toBe(`${expectedBasePath}/account/orders`))
113
+ const nav = within(screen.getByTestId('account-detail-nav'))
114
+ await user.click(nav.getByText('Addresses'))
115
+ await waitFor(() =>
116
+ expect(window.location.pathname).toBe(`${expectedBasePath}/account/addresses`)
117
+ )
118
+ await user.click(nav.getByText('Order History'))
119
+ await waitFor(() =>
120
+ expect(window.location.pathname).toBe(`${expectedBasePath}/account/orders`)
121
+ )
122
+ })
129
123
  })
130
124
 
131
125
  describe('Render and logs out', function () {
132
126
  test('Renders account detail page by default for logged-in customer, and can log out', async () => {
127
+ useCustomerType.mockReturnValue({isRegistered: true, isGuest: false})
128
+
133
129
  const {user} = renderWithProviders(<MockedComponent />)
134
130
 
135
131
  // Render user profile page
@@ -145,7 +141,10 @@ describe('Render and logs out', function () {
145
141
  })
146
142
 
147
143
  await user.click(screen.getAllByText(/Log Out/)[0])
144
+ useCustomerType.mockReturnValue({isRegistered: false, isGuest: true})
145
+
148
146
  await waitFor(() => {
147
+ expect(window.location.pathname).toBe(`${expectedBasePath}/login`)
149
148
  expect(screen.getByTestId('login-page')).toBeInTheDocument()
150
149
  })
151
150
  })
@@ -166,7 +165,7 @@ describe('updating profile', function () {
166
165
  )
167
166
  })
168
167
  test('Allows customer to edit profile details', async () => {
169
- sdk.useCustomerType.mockReturnValue({isRegistered: true, isExternal: false})
168
+ useCustomerType.mockReturnValue({isRegistered: true, isExternal: false})
170
169
  const {user} = renderWithProviders(<MockedComponent />)
171
170
  expect(await screen.findByTestId('account-page')).toBeInTheDocument()
172
171
  expect(await screen.findByTestId('account-detail-page')).toBeInTheDocument()
@@ -190,6 +189,7 @@ describe('updating profile', function () {
190
189
 
191
190
  describe('updating password', function () {
192
191
  beforeEach(() => {
192
+ useCustomerType.mockReturnValue({isRegistered: true, isExternal: false})
193
193
  global.server.use(
194
194
  rest.post('*/oauth2/token', (req, res, ctx) =>
195
195
  res(
@@ -35,6 +35,7 @@ import {heroFeatures, features} from '@salesforce/retail-react-app/app/pages/hom
35
35
 
36
36
  //Hooks
37
37
  import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
38
+ import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
38
39
 
39
40
  // Constants
40
41
  import {
@@ -55,6 +56,7 @@ import {useProductSearch} from '@salesforce/commerce-sdk-react'
55
56
  const Home = () => {
56
57
  const intl = useIntl()
57
58
  const einstein = useEinstein()
59
+ const dataCloud = useDataCloud()
58
60
  const {pathname} = useLocation()
59
61
 
60
62
  const {res} = useServerContext()
@@ -79,6 +81,7 @@ const Home = () => {
79
81
  /**************** Einstein ****************/
80
82
  useEffect(() => {
81
83
  einstein.sendViewPage(pathname)
84
+ dataCloud.sendViewPage(pathname)
82
85
  }, [])
83
86
 
84
87
  return (
@@ -23,6 +23,7 @@ import {useForm} from 'react-hook-form'
23
23
  import {useRouteMatch} from 'react-router'
24
24
  import {useLocation} from 'react-router-dom'
25
25
  import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
26
+ import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
26
27
  import LoginForm from '@salesforce/retail-react-app/app/components/login'
27
28
  import PasswordlessEmailConfirmation from '@salesforce/retail-react-app/app/components/email-confirmation/index'
28
29
  import {
@@ -56,6 +57,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
56
57
  const queryParams = new URLSearchParams(location.search)
57
58
  const {path} = useRouteMatch()
58
59
  const einstein = useEinstein()
60
+ const dataCloud = useDataCloud()
59
61
  const {isRegistered, customerType} = useCustomerType()
60
62
  const login = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C)
61
63
  const loginPasswordless = useAuthHelper(AuthHelpers.LoginPasswordlessUser)
@@ -184,6 +186,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
184
186
  /**************** Einstein ****************/
185
187
  useEffect(() => {
186
188
  einstein.sendViewPage(location.pathname)
189
+ dataCloud.sendViewPage(location.pathname)
187
190
  }, [])
188
191
 
189
192
  return (
@@ -31,6 +31,7 @@ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-curre
31
31
  import {useVariant} from '@salesforce/retail-react-app/app/hooks'
32
32
  import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
33
33
  import useEinstein from '@salesforce/retail-react-app/app/hooks/use-einstein'
34
+ import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
34
35
  import useActiveData from '@salesforce/retail-react-app/app/hooks/use-active-data'
35
36
  import {useServerContext} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
36
37
  // Project Components
@@ -61,6 +62,7 @@ const ProductDetail = () => {
61
62
  const history = useHistory()
62
63
  const location = useLocation()
63
64
  const einstein = useEinstein()
65
+ const dataCloud = useDataCloud()
64
66
  const activeData = useActiveData()
65
67
  const toast = useToast()
66
68
  const navigate = useNavigation()
@@ -99,7 +101,8 @@ const ProductDetail = () => {
99
101
  'prices',
100
102
  'variations',
101
103
  'set_products',
102
- 'bundled_products'
104
+ 'bundled_products',
105
+ 'page_meta_tags'
103
106
  ],
104
107
  allImages: true
105
108
  }
@@ -422,6 +425,7 @@ const ProductDetail = () => {
422
425
  useEffect(() => {
423
426
  if (product && product.type.set) {
424
427
  einstein.sendViewProduct(product)
428
+ dataCloud.sendViewProduct(product)
425
429
  const childrenProducts = product.setProducts
426
430
  childrenProducts.map((child) => {
427
431
  try {
@@ -433,6 +437,7 @@ const ProductDetail = () => {
433
437
  })
434
438
  }
435
439
  activeData.sendViewProduct(category, child, 'detail')
440
+ dataCloud.sendViewProduct(child)
436
441
  })
437
442
  } else if (product) {
438
443
  try {
@@ -444,6 +449,7 @@ const ProductDetail = () => {
444
449
  })
445
450
  }
446
451
  activeData.sendViewProduct(category, product, 'detail')
452
+ dataCloud.sendViewProduct(product)
447
453
  }
448
454
  }, [product])
449
455
 
@@ -455,7 +461,15 @@ const ProductDetail = () => {
455
461
  >
456
462
  <Helmet>
457
463
  <title>{product?.pageTitle}</title>
458
- <meta name="description" content={product?.pageDescription} />
464
+ {product?.pageMetaTags?.length > 0 &&
465
+ product.pageMetaTags.map(({id, value}) => (
466
+ <meta name={id} content={value} key={id} />
467
+ ))}
468
+ {/* Fallback for description if not included in pageMetaTags */}
469
+ {!product?.pageMetaTags?.some((tag) => tag.id === 'description') &&
470
+ product?.pageDescription && (
471
+ <meta name="description" content={product.pageDescription} />
472
+ )}
459
473
  </Helmet>
460
474
 
461
475
  <Stack spacing={16}>