@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.
- package/CHANGELOG.md +10 -0
- package/app/components/_app/index.jsx +13 -2
- package/app/components/_app/index.test.js +73 -29
- package/app/components/breadcrumb/index.jsx +2 -2
- package/app/components/links-list/index.test.js +0 -2
- package/app/components/product-view-modal/bundle.test.js +6 -1
- package/app/components/recommended-products/index.jsx +9 -0
- package/app/components/store-locator-modal/store-locator-content.test.jsx +0 -1
- package/app/hooks/use-datacloud.js +481 -0
- package/app/hooks/use-datacloud.test.js +164 -0
- package/app/mocks/datacloud-mock-data.js +404 -0
- package/app/pages/account/index.jsx +3 -0
- package/app/pages/account/index.test.js +36 -36
- package/app/pages/home/index.jsx +3 -0
- package/app/pages/login/index.jsx +3 -0
- package/app/pages/product-detail/index.jsx +16 -2
- package/app/pages/product-list/index.jsx +15 -1
- package/app/pages/registration/index.jsx +3 -0
- package/app/pages/reset-password/index.jsx +3 -0
- package/app/ssr.js +3 -1
- package/config/default.js +4 -0
- package/config/mocks/default.js +4 -0
- package/jest-setup.js +10 -0
- package/jest.config.js +5 -1
- package/package.json +15 -9
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025, Salesforce, 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 {useMemo} from 'react'
|
|
8
|
+
import Cookies from 'js-cookie'
|
|
9
|
+
import logger from '@salesforce/retail-react-app/app/utils/logger-instance'
|
|
10
|
+
import {initDataCloudSdk} from '@salesforce/cc-datacloud-typescript'
|
|
11
|
+
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
|
|
12
|
+
import {useUsid, useCustomerType, useDNT} from '@salesforce/commerce-sdk-react'
|
|
13
|
+
import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site'
|
|
14
|
+
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
|
|
15
|
+
|
|
16
|
+
export class DataCloudApi {
|
|
17
|
+
constructor({siteId, appSourceId, tenantId, dnt}) {
|
|
18
|
+
this.siteId = siteId
|
|
19
|
+
|
|
20
|
+
// Return early if Data Cloud API configuration is not available
|
|
21
|
+
if (!appSourceId || !tenantId) {
|
|
22
|
+
console.error('DataCloud API Configuration is missing.')
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
this.sdk = initDataCloudSdk(tenantId, appSourceId)
|
|
26
|
+
this.dnt = dnt
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Constructs the base event object with the necessary data required
|
|
31
|
+
* for every event sent to Data Cloud.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} args - The arguments containing event-specific details
|
|
34
|
+
* @returns {object} - The base event object
|
|
35
|
+
*/
|
|
36
|
+
_constructBaseEvent(args) {
|
|
37
|
+
return {
|
|
38
|
+
guestId: args.guestId,
|
|
39
|
+
siteId: this.siteId,
|
|
40
|
+
sessionId: args.sessionId,
|
|
41
|
+
deviceId: args.deviceId,
|
|
42
|
+
dateTime: new Date().toISOString(),
|
|
43
|
+
...(args.customerId && {customerId: args.customerId}), // Can remove the conditionality after the hook -> Promise is changed in future PWA release
|
|
44
|
+
...(args.customerNo && {customerNo: args.customerNo})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_constructUserDetails(args) {
|
|
49
|
+
return {
|
|
50
|
+
isAnonymous: args.isGuest,
|
|
51
|
+
firstName: args.firstName,
|
|
52
|
+
lastName: args.lastName
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generates the event details object required for sending an
|
|
58
|
+
* event to Data Cloud.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} eventType - The type of event being recorded (e.g
|
|
61
|
+
* "identity", "userEngagement", "contactPointEmail")
|
|
62
|
+
* @param {string} category - The category of the event, representing
|
|
63
|
+
* its broader grouping (e.g. "Profile", "Engagement")
|
|
64
|
+
* @returns {object} - The event details object
|
|
65
|
+
*/
|
|
66
|
+
_generateEventDetails(eventType, category, email = '') {
|
|
67
|
+
return {
|
|
68
|
+
eventId: crypto.randomUUID(),
|
|
69
|
+
eventType: eventType,
|
|
70
|
+
category: category,
|
|
71
|
+
...(eventType === 'contactPointEmail' && {email})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Constructs an object containing the product Id.
|
|
77
|
+
*
|
|
78
|
+
* This method extracts and returns the appropriate product Id based on
|
|
79
|
+
* the product type.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} product - The product object
|
|
82
|
+
* @returns {object} - An object containing the resolved product Id
|
|
83
|
+
*/
|
|
84
|
+
_constructDatacloudProduct(product) {
|
|
85
|
+
// Return the product SKU in the following priority order:
|
|
86
|
+
// 1. id if available - SKU of the Variant Product
|
|
87
|
+
// 2. productId if available - SKU of product hits within a category
|
|
88
|
+
// 3. masterId - SKU of the Master Product
|
|
89
|
+
return {
|
|
90
|
+
id: product?.id ?? product?.productId ?? product?.master?.masterId
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Constructs the base search result object with relevant search
|
|
95
|
+
* metadata.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} searchParams - The searchParams object
|
|
98
|
+
* @returns {object} - The base search result object
|
|
99
|
+
*/
|
|
100
|
+
_constructBaseSearchResult(searchParams) {
|
|
101
|
+
return {
|
|
102
|
+
searchResultTitle: searchParams.q,
|
|
103
|
+
searchResultPosition: searchParams.offset,
|
|
104
|
+
searchResultPageNumber:
|
|
105
|
+
searchParams.limit != 0 ? searchParams.offset / searchParams.limit + 1 : 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_concatenateEvents = (...events) => ({...events.reduce((acc, obj) => ({...acc, ...obj}), {})})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Sends a `page-view` event to Data Cloud.
|
|
113
|
+
*
|
|
114
|
+
* This method records an `userEnagement` event type to track which page the shopper has viewed.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} path - The URL path of the page that was viewed
|
|
117
|
+
* @param {object} args - Additional metadata for the event
|
|
118
|
+
*/
|
|
119
|
+
async sendViewPage(path, args) {
|
|
120
|
+
const baseEvent = this._constructBaseEvent(args)
|
|
121
|
+
const userDetails = this._constructUserDetails(args)
|
|
122
|
+
|
|
123
|
+
// If DNT, we do not send the identity Profile event
|
|
124
|
+
const identityProfile = this.dnt
|
|
125
|
+
? {}
|
|
126
|
+
: this._concatenateEvents(
|
|
127
|
+
baseEvent,
|
|
128
|
+
this._generateEventDetails('identity', 'Profile'),
|
|
129
|
+
userDetails,
|
|
130
|
+
{
|
|
131
|
+
sourceUrl: path
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const userEngagement = this._concatenateEvents(
|
|
136
|
+
baseEvent,
|
|
137
|
+
this._generateEventDetails('userEngagement', 'Engagement'),
|
|
138
|
+
{
|
|
139
|
+
interactionName: 'page-view',
|
|
140
|
+
sourceUrl: path
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const interaction = {
|
|
145
|
+
events: [...(!this.dnt ? [identityProfile] : []), userEngagement]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
this.sdk.webEventsAppSourceIdPost(interaction)
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.error('Error sending DataCloud event', err)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Sends a `catalog-object-view-start` event to Data Cloud.
|
|
157
|
+
*
|
|
158
|
+
* This method records a `catalog` event type to track when a shopper
|
|
159
|
+
* views the details of a product (e.g. a Product Detail Page).
|
|
160
|
+
*
|
|
161
|
+
* @param {object} product - The product being viewed
|
|
162
|
+
* @param {object} args - Additional metadata for the event
|
|
163
|
+
*/
|
|
164
|
+
async sendViewProduct(product, args) {
|
|
165
|
+
const baseEvent = this._constructBaseEvent(args)
|
|
166
|
+
const baseProduct = this._constructDatacloudProduct(product)
|
|
167
|
+
const userDetails = this._constructUserDetails(args)
|
|
168
|
+
|
|
169
|
+
const identityProfile = this.dnt
|
|
170
|
+
? {}
|
|
171
|
+
: this._concatenateEvents(
|
|
172
|
+
baseEvent,
|
|
173
|
+
this._generateEventDetails('identity', 'Profile'),
|
|
174
|
+
userDetails
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
let contactPointEmail = null
|
|
178
|
+
if (args.email) {
|
|
179
|
+
contactPointEmail = this._concatenateEvents(
|
|
180
|
+
baseEvent,
|
|
181
|
+
this._generateEventDetails('contactPointEmail', 'Profile', args.email)
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const catalog = this._concatenateEvents(
|
|
186
|
+
baseEvent,
|
|
187
|
+
this._generateEventDetails('catalog', 'Engagement'),
|
|
188
|
+
baseProduct,
|
|
189
|
+
{
|
|
190
|
+
type: 'Product',
|
|
191
|
+
webStoreId: 'pwa',
|
|
192
|
+
interactionName: 'catalog-object-view-start'
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const interaction = {
|
|
197
|
+
events: [
|
|
198
|
+
...(!this.dnt ? [identityProfile] : []),
|
|
199
|
+
...(contactPointEmail ? [contactPointEmail] : []),
|
|
200
|
+
catalog
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
this.sdk.webEventsAppSourceIdPost(interaction)
|
|
206
|
+
} catch (err) {
|
|
207
|
+
logger.error('Error sending DataCloud event', err)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Sends a `catalog-object-impression` event to Data Cloud.
|
|
213
|
+
*
|
|
214
|
+
* This method records a `catalog` event type and represents a single
|
|
215
|
+
* page of product impressions (e.g. a Product List Page).
|
|
216
|
+
*
|
|
217
|
+
* One event is sent for each product on the page.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} searchParams - The searchParams object
|
|
220
|
+
* @param {object} category - The category object
|
|
221
|
+
* @param {object} searchResults - The searchResults object
|
|
222
|
+
* @param {object} args - Additional metadata for the event
|
|
223
|
+
*/
|
|
224
|
+
async sendViewCategory(searchParams, category, searchResults, args) {
|
|
225
|
+
const baseEvent = this._constructBaseEvent(args)
|
|
226
|
+
const userDetails = this._constructUserDetails(args)
|
|
227
|
+
|
|
228
|
+
const products = searchResults?.hits?.map((product) =>
|
|
229
|
+
this._constructDatacloudProduct(product)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const catalogObjects = products.map((product) => {
|
|
233
|
+
return this._concatenateEvents(
|
|
234
|
+
baseEvent,
|
|
235
|
+
this._generateEventDetails('catalog', 'Engagement'),
|
|
236
|
+
this._constructBaseSearchResult(searchParams),
|
|
237
|
+
{
|
|
238
|
+
id: product.id,
|
|
239
|
+
type: 'Product',
|
|
240
|
+
webStoreId: 'pwa',
|
|
241
|
+
categoryId: category.id,
|
|
242
|
+
interactionName: 'catalog-object-impression'
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const identityProfile = this.dnt
|
|
248
|
+
? null
|
|
249
|
+
: this._concatenateEvents(
|
|
250
|
+
baseEvent,
|
|
251
|
+
this._generateEventDetails('identity', 'Profile'),
|
|
252
|
+
userDetails
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
let contactPointEmail = null
|
|
256
|
+
if (args.email) {
|
|
257
|
+
contactPointEmail = this._concatenateEvents(
|
|
258
|
+
baseEvent,
|
|
259
|
+
this._generateEventDetails('contactPointEmail', 'Profile', args.email)
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const interaction = {
|
|
264
|
+
events: [
|
|
265
|
+
...(!this.dnt ? [identityProfile] : []),
|
|
266
|
+
...(contactPointEmail ? [contactPointEmail] : []),
|
|
267
|
+
...catalogObjects
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
this.sdk.webEventsAppSourceIdPost(interaction)
|
|
273
|
+
} catch (err) {
|
|
274
|
+
logger.error('Error sending DataCloud event', err)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sends a `catalog-object-impression` event to Data Cloud with
|
|
280
|
+
* additional search result data.
|
|
281
|
+
*
|
|
282
|
+
* This method records a `catalog` event type when a shopper completes a
|
|
283
|
+
* search, logging an impression of the products displayed in the search
|
|
284
|
+
* results.
|
|
285
|
+
*
|
|
286
|
+
* @param {object} searchParams - The searchParams object
|
|
287
|
+
* @param {object} searchResults - The searchResults object containing an
|
|
288
|
+
* array of product impressions
|
|
289
|
+
* @param {object} args - Additional metadata for the event
|
|
290
|
+
*/
|
|
291
|
+
async sendViewSearchResults(searchParams, searchResults, args) {
|
|
292
|
+
const baseEvent = this._constructBaseEvent(args)
|
|
293
|
+
const userDetails = this._constructUserDetails(args)
|
|
294
|
+
|
|
295
|
+
const products = searchResults?.hits?.map((product) =>
|
|
296
|
+
this._constructDatacloudProduct(product)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const catalogObjects = products.map((product) => {
|
|
300
|
+
return this._concatenateEvents(
|
|
301
|
+
baseEvent,
|
|
302
|
+
this._generateEventDetails('catalog', 'Engagement'),
|
|
303
|
+
this._constructBaseSearchResult(searchParams),
|
|
304
|
+
{
|
|
305
|
+
searchResultId: crypto.randomUUID(),
|
|
306
|
+
id: product.id,
|
|
307
|
+
type: 'Product',
|
|
308
|
+
webStoreId: 'pwa',
|
|
309
|
+
interactionName: 'catalog-object-impression'
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const identityProfile = this.dnt
|
|
315
|
+
? {}
|
|
316
|
+
: this._concatenateEvents(
|
|
317
|
+
baseEvent,
|
|
318
|
+
this._generateEventDetails('identity', 'Profile'),
|
|
319
|
+
userDetails
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
let contactPointEmail = null
|
|
323
|
+
if (args.email) {
|
|
324
|
+
contactPointEmail = this._concatenateEvents(
|
|
325
|
+
baseEvent,
|
|
326
|
+
this._generateEventDetails('contactPointEmail', 'Profile', args.email)
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const interaction = {
|
|
331
|
+
events: [
|
|
332
|
+
...(!this.dnt ? [identityProfile] : []),
|
|
333
|
+
...(contactPointEmail ? [contactPointEmail] : []),
|
|
334
|
+
...catalogObjects
|
|
335
|
+
]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
this.sdk.webEventsAppSourceIdPost(interaction)
|
|
340
|
+
} catch (err) {
|
|
341
|
+
logger.error('Error sending DataCloud event', err)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Sends a `catalog-object-impression` event to Data Cloud with
|
|
347
|
+
* additional recommendation data.
|
|
348
|
+
*
|
|
349
|
+
* This method records a `catalog` event type when a shopper views a recommendation,
|
|
350
|
+
* logging an impression of the products displayed in the recommendation.
|
|
351
|
+
*
|
|
352
|
+
* @param {object} recommenderDetails - Metadata about the recommendation source
|
|
353
|
+
* @param {array} products - List of recommended products
|
|
354
|
+
* @param {object} args - Additional metadata for the event
|
|
355
|
+
*/
|
|
356
|
+
async sendViewRecommendations(recommenderDetails, products, args) {
|
|
357
|
+
const baseEvent = this._constructBaseEvent(args)
|
|
358
|
+
const userDetails = this._constructUserDetails(args)
|
|
359
|
+
|
|
360
|
+
const catalogObjects = products.map((product) => {
|
|
361
|
+
return this._concatenateEvents(
|
|
362
|
+
baseEvent,
|
|
363
|
+
this._generateEventDetails('catalog', 'Engagement'),
|
|
364
|
+
{
|
|
365
|
+
id: product.id,
|
|
366
|
+
type: 'Product',
|
|
367
|
+
webStoreId: 'pwa',
|
|
368
|
+
interactionName: 'catalog-object-impression',
|
|
369
|
+
personalizationId: recommenderDetails.recommenderName, //* The identifier of the personalization (e.g., recommendation), provided by the personalization service provider, that led to the event.
|
|
370
|
+
personalizationContextId: recommenderDetails.__recoUUID //* The identifier, provided by the personalization service provider, of the specific content (e.g., product) associated with this event.
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const identityProfile = this.dnt
|
|
376
|
+
? {}
|
|
377
|
+
: this._concatenateEvents(
|
|
378
|
+
baseEvent,
|
|
379
|
+
this._generateEventDetails('identity', 'Profile'),
|
|
380
|
+
userDetails
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
let contactPointEmail = null
|
|
384
|
+
if (args.email) {
|
|
385
|
+
contactPointEmail = this._concatenateEvents(
|
|
386
|
+
baseEvent,
|
|
387
|
+
this._generateEventDetails('contactPointEmail', 'Profile', args.email)
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const interaction = {
|
|
392
|
+
events: [
|
|
393
|
+
...(!this.dnt ? [identityProfile] : []),
|
|
394
|
+
...(contactPointEmail ? [contactPointEmail] : []),
|
|
395
|
+
...catalogObjects
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
this.sdk.webEventsAppSourceIdPost(interaction)
|
|
401
|
+
} catch (err) {
|
|
402
|
+
logger.error('Error sending DataCloud event', err)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Custom hook for sending PWA Kit events to Data Cloud.
|
|
409
|
+
*
|
|
410
|
+
* This hook provides methods to track various user interactions, such as
|
|
411
|
+
* page views, product views, category views, search impressions, and recommendations.
|
|
412
|
+
*
|
|
413
|
+
* @returns {object} An object containing methods for sending different Data Cloud events
|
|
414
|
+
*/
|
|
415
|
+
const useDataCloud = () => {
|
|
416
|
+
const {getUsidWhenReady} = useUsid()
|
|
417
|
+
const {isRegistered} = useCustomerType()
|
|
418
|
+
const {data: customer} = useCurrentCustomer()
|
|
419
|
+
const {site} = useMultiSite()
|
|
420
|
+
const {effectiveDnt} = useDNT()
|
|
421
|
+
const sessionId = Cookies.get('sid')
|
|
422
|
+
|
|
423
|
+
// If Do Not Track is enabled, then the following fields are replaced with '__DNT__'
|
|
424
|
+
const getEventUserParameters = async () => {
|
|
425
|
+
const usid = await getUsidWhenReady()
|
|
426
|
+
return {
|
|
427
|
+
isGuest: isRegistered ? 0 : 1,
|
|
428
|
+
customerId: effectiveDnt ? '__DNT__' : customer?.customerId,
|
|
429
|
+
customerNo: effectiveDnt ? '__DNT__' : customer?.customerNo,
|
|
430
|
+
guestId: effectiveDnt ? '__DNT__' : usid,
|
|
431
|
+
deviceId: effectiveDnt ? '__DNT__' : usid,
|
|
432
|
+
sessionId: effectiveDnt ? '__DNT__' : sessionId,
|
|
433
|
+
firstName: customer?.firstName,
|
|
434
|
+
lastName: customer?.lastName,
|
|
435
|
+
email: !effectiveDnt ? customer?.email : undefined
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Grab Data Cloud configuration values and intialize the sdk
|
|
440
|
+
const {
|
|
441
|
+
app: {dataCloudAPI: config}
|
|
442
|
+
} = getConfig()
|
|
443
|
+
|
|
444
|
+
const {appSourceId, tenantId} = config
|
|
445
|
+
|
|
446
|
+
const dataCloud = useMemo(
|
|
447
|
+
() =>
|
|
448
|
+
new DataCloudApi({
|
|
449
|
+
siteId: site.id,
|
|
450
|
+
appSourceId: appSourceId,
|
|
451
|
+
tenantId: tenantId,
|
|
452
|
+
dnt: effectiveDnt
|
|
453
|
+
}),
|
|
454
|
+
[site]
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
async sendViewPage(...args) {
|
|
459
|
+
const userParameters = await getEventUserParameters()
|
|
460
|
+
return dataCloud.sendViewPage(...args.concat(userParameters))
|
|
461
|
+
},
|
|
462
|
+
async sendViewProduct(...args) {
|
|
463
|
+
const userParameters = await getEventUserParameters()
|
|
464
|
+
return dataCloud.sendViewProduct(...args.concat(userParameters))
|
|
465
|
+
},
|
|
466
|
+
async sendViewCategory(...args) {
|
|
467
|
+
const userParameters = await getEventUserParameters()
|
|
468
|
+
return dataCloud.sendViewCategory(...args.concat(userParameters))
|
|
469
|
+
},
|
|
470
|
+
async sendViewSearchResults(...args) {
|
|
471
|
+
const userParameters = await getEventUserParameters()
|
|
472
|
+
return dataCloud.sendViewSearchResults(...args.concat(userParameters))
|
|
473
|
+
},
|
|
474
|
+
async sendViewRecommendations(...args) {
|
|
475
|
+
const userParameters = await getEventUserParameters()
|
|
476
|
+
return dataCloud.sendViewRecommendations(...args.concat(userParameters))
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export default useDataCloud
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import {renderHook, waitFor} from '@testing-library/react'
|
|
10
|
+
import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
|
|
11
|
+
import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
|
|
12
|
+
import {useDNT} from '@salesforce/commerce-sdk-react'
|
|
13
|
+
import {
|
|
14
|
+
mockLoginViewPageEvent,
|
|
15
|
+
mockViewProductEvent,
|
|
16
|
+
mockViewCategoryEvent,
|
|
17
|
+
mockViewSearchResultsEvent,
|
|
18
|
+
mockViewRecommendationsEvent,
|
|
19
|
+
mockSearchParam,
|
|
20
|
+
mockGloveSearchResult,
|
|
21
|
+
mockCategorySearchParams,
|
|
22
|
+
mockRecommendationIds,
|
|
23
|
+
mockLoginViewPageEventDNT
|
|
24
|
+
} from '@salesforce/retail-react-app/app/mocks/datacloud-mock-data'
|
|
25
|
+
import {
|
|
26
|
+
mockProduct,
|
|
27
|
+
mockCategory,
|
|
28
|
+
mockSearchResults,
|
|
29
|
+
mockRecommenderDetails
|
|
30
|
+
} from '@salesforce/retail-react-app/app/hooks/einstein-mock-data'
|
|
31
|
+
|
|
32
|
+
const dataCloudConfig = {
|
|
33
|
+
app: {
|
|
34
|
+
dataCloudAPI: {
|
|
35
|
+
appSourceId: '6ebc532a-2247-48e9-8300-d8c2b84eb463',
|
|
36
|
+
tenantId: 'mvst0mlfmrsd8zbwg8zgmytbg1'
|
|
37
|
+
},
|
|
38
|
+
defaultSite: 'test-site'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => {
|
|
43
|
+
return {
|
|
44
|
+
getConfig: jest.fn(() => dataCloudConfig)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
jest.mock('@salesforce/commerce-sdk-react', () => {
|
|
49
|
+
const originalModule = jest.requireActual('@salesforce/commerce-sdk-react')
|
|
50
|
+
return {
|
|
51
|
+
...originalModule,
|
|
52
|
+
useUsid: () => {
|
|
53
|
+
return {
|
|
54
|
+
getUsidWhenReady: jest.fn(() => {
|
|
55
|
+
return 'guest-usid'
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
useCustomerType: jest.fn(() => {
|
|
60
|
+
return {isRegistered: true}
|
|
61
|
+
}),
|
|
62
|
+
useDNT: jest.fn(() => {
|
|
63
|
+
return {effectiveDnt: false}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
jest.mock('@salesforce/retail-react-app/app/hooks/use-current-customer', () => ({
|
|
69
|
+
useCurrentCustomer: jest.fn(() => {
|
|
70
|
+
return {
|
|
71
|
+
data: {
|
|
72
|
+
customerId: 1234567890,
|
|
73
|
+
firstName: 'John',
|
|
74
|
+
lastName: 'Smith',
|
|
75
|
+
email: 'johnsmith@salesforce.com'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}))
|
|
80
|
+
jest.mock('js-cookie', () => ({
|
|
81
|
+
get: jest.fn(() => 'mockCookieValue')
|
|
82
|
+
}))
|
|
83
|
+
const mockWebEventsAppSourceIdPost = jest.fn()
|
|
84
|
+
jest.mock('@salesforce/cc-datacloud-typescript', () => {
|
|
85
|
+
return {
|
|
86
|
+
initDataCloudSdk: () => {
|
|
87
|
+
return {
|
|
88
|
+
webEventsAppSourceIdPost: mockWebEventsAppSourceIdPost
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const mockUseContext = jest.fn().mockImplementation(() => ({site: {id: 'RefArch'}}))
|
|
95
|
+
React.useContext = mockUseContext
|
|
96
|
+
describe('useDataCloud', function () {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
jest.clearAllMocks()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('sendViewPage', async () => {
|
|
102
|
+
const {result} = renderHook(() => useDataCloud())
|
|
103
|
+
expect(result.current).toBeDefined()
|
|
104
|
+
result.current.sendViewPage('/login')
|
|
105
|
+
await waitFor(() => {
|
|
106
|
+
expect(mockWebEventsAppSourceIdPost).toHaveBeenCalledWith(mockLoginViewPageEvent)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('sendViewPage does not send Profile event when DNT is enabled', async () => {
|
|
111
|
+
useDNT.mockReturnValueOnce({
|
|
112
|
+
effectiveDnt: true
|
|
113
|
+
})
|
|
114
|
+
const {result} = renderHook(() => useDataCloud())
|
|
115
|
+
expect(result.current).toBeDefined()
|
|
116
|
+
result.current.sendViewPage('/login')
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(mockWebEventsAppSourceIdPost).toHaveBeenCalledWith(mockLoginViewPageEventDNT)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('sendViewProduct', async () => {
|
|
123
|
+
const {result} = renderHook(() => useDataCloud())
|
|
124
|
+
expect(result.current).toBeDefined()
|
|
125
|
+
result.current.sendViewProduct(mockProduct)
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(mockWebEventsAppSourceIdPost).toHaveBeenCalledWith(mockViewProductEvent)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('sendViewCategory with no email', async () => {
|
|
132
|
+
useCurrentCustomer.mockReturnValue({
|
|
133
|
+
data: {
|
|
134
|
+
customerId: 1234567890,
|
|
135
|
+
firstName: 'John',
|
|
136
|
+
lastName: 'Smith'
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
const {result} = renderHook(() => useDataCloud())
|
|
140
|
+
expect(result.current).toBeDefined()
|
|
141
|
+
result.current.sendViewCategory(mockCategorySearchParams, mockCategory, mockSearchResults)
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(mockWebEventsAppSourceIdPost).toHaveBeenCalledWith(mockViewCategoryEvent)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('sendViewSearchResults with no email', async () => {
|
|
148
|
+
const {result} = renderHook(() => useDataCloud())
|
|
149
|
+
expect(result.current).toBeDefined()
|
|
150
|
+
result.current.sendViewSearchResults(mockSearchParam, mockGloveSearchResult)
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(mockWebEventsAppSourceIdPost).toHaveBeenCalledWith(mockViewSearchResultsEvent)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('sendViewRecommendations with non email', async () => {
|
|
157
|
+
const {result} = renderHook(() => useDataCloud())
|
|
158
|
+
expect(result.current).toBeDefined()
|
|
159
|
+
result.current.sendViewRecommendations(mockRecommenderDetails, mockRecommendationIds)
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(mockWebEventsAppSourceIdPost).toHaveBeenCalledWith(mockViewRecommendationsEvent)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|