@salesforce/retail-react-app 7.0.0-preview.0 → 7.0.0
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 +9 -8
- package/app/components/dynamic-image/index.jsx +91 -16
- package/app/components/dynamic-image/index.test.js +214 -30
- package/app/components/image/index.jsx +5 -13
- package/app/components/image/index.test.js +6 -3
- package/app/components/island/README.md +15 -10
- package/app/components/island/index.jsx +12 -5
- package/app/components/island/index.test.js +35 -0
- package/app/components/passwordless-login/index.jsx +4 -5
- package/app/components/passwordless-login/index.test.js +2 -4
- package/app/components/product-tile/index.jsx +1 -1
- package/app/components/product-view-modal/bundle.jsx +12 -2
- package/app/components/social-login/index.jsx +1 -0
- package/app/components/standard-login/index.jsx +4 -1
- package/app/constants.js +3 -0
- package/app/hooks/use-auth-modal.js +68 -67
- package/app/hooks/use-auth-modal.test.js +93 -23
- package/app/hooks/use-datacloud.js +169 -192
- package/app/hooks/use-datacloud.test.js +273 -17
- package/app/pages/cart/index.jsx +2 -1
- package/app/pages/cart/partials/cart-secondary-button-group.jsx +8 -10
- package/app/pages/cart/partials/cart-secondary-button-group.test.js +2 -3
- package/app/pages/checkout/partials/contact-info.jsx +9 -8
- package/app/pages/checkout/partials/contact-info.test.js +41 -4
- package/app/pages/checkout/partials/login-state.jsx +3 -3
- package/app/pages/home/index.test.js +2 -1
- package/app/pages/login/index.jsx +37 -37
- package/app/pages/login/index.test.js +42 -0
- package/app/pages/product-detail/index.jsx +64 -73
- package/app/pages/product-list/index.jsx +19 -9
- package/app/pages/product-list/index.test.js +153 -19
- package/app/utils/image.js +29 -0
- package/app/utils/image.test.js +141 -1
- package/app/utils/responsive-image.js +197 -115
- package/app/utils/responsive-image.test.js +483 -133
- package/config/default.js +2 -2
- package/config/mocks/default.js +2 -2
- package/package.json +7 -7
|
@@ -50,7 +50,8 @@ const mockRegisteredCustomer = {
|
|
|
50
50
|
|
|
51
51
|
const mockAuthHelperFunctions = {
|
|
52
52
|
[AuthHelpers.AuthorizePasswordless]: {mutateAsync: jest.fn()},
|
|
53
|
-
[AuthHelpers.Register]: {mutateAsync: jest.fn()}
|
|
53
|
+
[AuthHelpers.Register]: {mutateAsync: jest.fn()},
|
|
54
|
+
[AuthHelpers.LoginRegisteredUserB2C]: {mutateAsync: jest.fn()}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
jest.mock('@salesforce/commerce-sdk-react', () => {
|
|
@@ -119,6 +120,7 @@ beforeEach(() => {
|
|
|
119
120
|
afterEach(() => {
|
|
120
121
|
localStorage.clear()
|
|
121
122
|
jest.resetModules()
|
|
123
|
+
jest.restoreAllMocks()
|
|
122
124
|
})
|
|
123
125
|
|
|
124
126
|
test('Renders login modal by default', async () => {
|
|
@@ -171,11 +173,41 @@ test('Renders check email modal on email mode', async () => {
|
|
|
171
173
|
mockUseForm.mockRestore()
|
|
172
174
|
})
|
|
173
175
|
|
|
176
|
+
test('allows regular login via Enter key in password mode', async () => {
|
|
177
|
+
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
|
|
178
|
+
const validEmail = 'test@salesforce.com'
|
|
179
|
+
const validPassword = 'Password123!'
|
|
180
|
+
|
|
181
|
+
// open the modal
|
|
182
|
+
const trigger = screen.getByText(/open modal/i)
|
|
183
|
+
await user.click(trigger)
|
|
184
|
+
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(screen.getByText(/continue securely/i)).toBeInTheDocument()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// enter email and switch to password mode
|
|
190
|
+
await user.type(screen.getByLabelText('Email'), validEmail)
|
|
191
|
+
await user.click(screen.getByText(/password/i))
|
|
192
|
+
|
|
193
|
+
// enter password
|
|
194
|
+
await user.type(screen.getByLabelText('Password'), validPassword)
|
|
195
|
+
|
|
196
|
+
// simulate Enter key press in password field
|
|
197
|
+
await user.keyboard('{Enter}')
|
|
198
|
+
|
|
199
|
+
// should trigger regular login
|
|
200
|
+
expect(
|
|
201
|
+
mockAuthHelperFunctions[AuthHelpers.LoginRegisteredUserB2C].mutateAsync
|
|
202
|
+
).toHaveBeenCalledWith({
|
|
203
|
+
username: validEmail,
|
|
204
|
+
password: validPassword
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
174
208
|
describe('Passwordless enabled', () => {
|
|
175
209
|
test('Renders passwordless login when enabled', async () => {
|
|
176
|
-
const user =
|
|
177
|
-
|
|
178
|
-
renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
|
|
210
|
+
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
|
|
179
211
|
|
|
180
212
|
// open the modal
|
|
181
213
|
const trigger = screen.getByText(/open modal/i)
|
|
@@ -187,6 +219,10 @@ describe('Passwordless enabled', () => {
|
|
|
187
219
|
})
|
|
188
220
|
|
|
189
221
|
test('Allows passwordless login', async () => {
|
|
222
|
+
jest.spyOn(window, 'location', 'get').mockReturnValue({
|
|
223
|
+
pathname: '/',
|
|
224
|
+
origin: 'https://example.com'
|
|
225
|
+
})
|
|
190
226
|
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
|
|
191
227
|
const validEmail = 'test@salesforce.com'
|
|
192
228
|
|
|
@@ -203,8 +239,6 @@ describe('Passwordless enabled', () => {
|
|
|
203
239
|
|
|
204
240
|
// initiate passwordless login
|
|
205
241
|
const passwordlessLoginButton = screen.getByText(/continue securely/i)
|
|
206
|
-
// Click the button twice as the isPasswordlessLoginClicked state doesn't change after the first click
|
|
207
|
-
await user.click(passwordlessLoginButton)
|
|
208
242
|
await user.click(passwordlessLoginButton)
|
|
209
243
|
expect(
|
|
210
244
|
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
|
|
@@ -214,20 +248,64 @@ describe('Passwordless enabled', () => {
|
|
|
214
248
|
})
|
|
215
249
|
|
|
216
250
|
// check that check email modal is open
|
|
251
|
+
await waitFor(
|
|
252
|
+
() => {
|
|
253
|
+
const withinForm = within(screen.getByTestId('sf-form-resend-passwordless-email'))
|
|
254
|
+
expect(withinForm.getByText(/Check Your Email/i)).toBeInTheDocument()
|
|
255
|
+
expect(withinForm.getByText(validEmail)).toBeInTheDocument()
|
|
256
|
+
},
|
|
257
|
+
{timeout: 5000}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
// resend the email
|
|
261
|
+
await user.click(screen.getByText(/Resend Link/i))
|
|
262
|
+
expect(
|
|
263
|
+
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
|
|
264
|
+
).toHaveBeenCalledWith({
|
|
265
|
+
userid: validEmail,
|
|
266
|
+
callbackURI: 'https://webhook.site/27761b71-50c1-4097-a600-21a3b89a546c?redirectUrl=/'
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('allows passwordless login via Enter key', async () => {
|
|
271
|
+
jest.spyOn(window, 'location', 'get').mockReturnValue({
|
|
272
|
+
pathname: '/',
|
|
273
|
+
origin: 'https://example.com'
|
|
274
|
+
})
|
|
275
|
+
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
|
|
276
|
+
const validEmail = 'test@salesforce.com'
|
|
277
|
+
|
|
278
|
+
// open the modal
|
|
279
|
+
const trigger = screen.getByText(/open modal/i)
|
|
280
|
+
await user.click(trigger)
|
|
281
|
+
|
|
217
282
|
await waitFor(() => {
|
|
218
|
-
|
|
219
|
-
expect(withinForm.getByText(/Check Your Email/i)).toBeInTheDocument()
|
|
220
|
-
expect(withinForm.getByText(validEmail)).toBeInTheDocument()
|
|
283
|
+
expect(screen.getByText(/continue securely/i)).toBeInTheDocument()
|
|
221
284
|
})
|
|
222
285
|
|
|
223
|
-
//
|
|
224
|
-
user.
|
|
286
|
+
// enter a valid email address
|
|
287
|
+
await user.type(screen.getByLabelText('Email'), validEmail)
|
|
288
|
+
|
|
289
|
+
// simulate Enter key press in email field
|
|
290
|
+
await user.keyboard('{Enter}')
|
|
291
|
+
|
|
292
|
+
// should trigger passwordless login
|
|
225
293
|
expect(
|
|
226
294
|
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
|
|
227
295
|
).toHaveBeenCalledWith({
|
|
228
296
|
userid: validEmail,
|
|
229
297
|
callbackURI: 'https://webhook.site/27761b71-50c1-4097-a600-21a3b89a546c?redirectUrl=/'
|
|
230
298
|
})
|
|
299
|
+
|
|
300
|
+
// check that check email modal is open
|
|
301
|
+
await waitFor(
|
|
302
|
+
() => {
|
|
303
|
+
const withinForm = within(screen.getByTestId('sf-form-resend-passwordless-email'))
|
|
304
|
+
expect(withinForm.getByText(/Check Your Email/i)).toBeInTheDocument()
|
|
305
|
+
expect(withinForm.getByText(validEmail)).toBeInTheDocument()
|
|
306
|
+
},
|
|
307
|
+
{timeout: 5000}
|
|
308
|
+
)
|
|
231
309
|
})
|
|
232
310
|
})
|
|
233
311
|
|
|
@@ -277,10 +355,8 @@ test.skip('Renders error when given incorrect log in credentials', async () => {
|
|
|
277
355
|
})
|
|
278
356
|
|
|
279
357
|
test('Allows customer to create an account', async () => {
|
|
280
|
-
const user = userEvent.setup()
|
|
281
|
-
|
|
282
358
|
// render our test component
|
|
283
|
-
renderWithProviders(<MockedComponent />, {
|
|
359
|
+
const {user} = renderWithProviders(<MockedComponent />, {
|
|
284
360
|
wrapperProps: {
|
|
285
361
|
bypassAuth: true
|
|
286
362
|
}
|
|
@@ -360,10 +436,8 @@ test('Allows customer to create an account', async () => {
|
|
|
360
436
|
// TODO: investigate why this test is failing when running with other tests
|
|
361
437
|
// eslint-disable-next-line jest/no-disabled-tests
|
|
362
438
|
test.skip('Allows customer to sign in to their account', async () => {
|
|
363
|
-
const user = userEvent.setup()
|
|
364
|
-
|
|
365
439
|
// render our test component
|
|
366
|
-
renderWithProviders(<MockedComponent />, {
|
|
440
|
+
const {user} = renderWithProviders(<MockedComponent />, {
|
|
367
441
|
wrapperProps: {
|
|
368
442
|
bypassAuth: false
|
|
369
443
|
}
|
|
@@ -418,10 +492,8 @@ describe('Reset password', function () {
|
|
|
418
492
|
// TODO: Fix flaky/broken test
|
|
419
493
|
// eslint-disable-next-line jest/no-disabled-tests
|
|
420
494
|
test.skip('Allows customer to generate password token', async () => {
|
|
421
|
-
const user = userEvent.setup()
|
|
422
|
-
|
|
423
495
|
// render our test component
|
|
424
|
-
renderWithProviders(<MockedComponent initialView="password" />, {
|
|
496
|
+
const {user} = renderWithProviders(<MockedComponent initialView="password" />, {
|
|
425
497
|
wrapperProps: {
|
|
426
498
|
bypassAuth: false
|
|
427
499
|
}
|
|
@@ -451,10 +523,8 @@ describe('Reset password', function () {
|
|
|
451
523
|
// TODO: Fix flaky/broken test
|
|
452
524
|
// eslint-disable-next-line jest/no-disabled-tests
|
|
453
525
|
test.skip('Allows customer to open generate password token modal from everywhere', async () => {
|
|
454
|
-
const user = userEvent.setup()
|
|
455
|
-
|
|
456
526
|
// render our test component
|
|
457
|
-
renderWithProviders(<MockedComponent initialView="password" />)
|
|
527
|
+
const {user} = renderWithProviders(<MockedComponent initialView="password" />)
|
|
458
528
|
|
|
459
529
|
// open the modal
|
|
460
530
|
const trigger = screen.getByText(/open modal/i)
|
|
@@ -16,12 +16,6 @@ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-cur
|
|
|
16
16
|
export class DataCloudApi {
|
|
17
17
|
constructor({siteId, appSourceId, tenantId, dnt}) {
|
|
18
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
19
|
this.sdk = initDataCloudSdk(tenantId, appSourceId)
|
|
26
20
|
this.dnt = dnt
|
|
27
21
|
}
|
|
@@ -38,13 +32,20 @@ export class DataCloudApi {
|
|
|
38
32
|
guestId: args.guestId,
|
|
39
33
|
siteId: this.siteId,
|
|
40
34
|
sessionId: args.sessionId,
|
|
41
|
-
deviceId: args.
|
|
35
|
+
deviceId: args.customerId || args.guestId,
|
|
42
36
|
dateTime: new Date().toISOString(),
|
|
43
37
|
...(args.customerId && {customerId: args.customerId}), // Can remove the conditionality after the hook -> Promise is changed in future PWA release
|
|
44
38
|
...(args.customerNo && {customerNo: args.customerNo})
|
|
45
39
|
}
|
|
46
40
|
}
|
|
47
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Constructs a user details object for use in identity events.
|
|
44
|
+
* Includes information such as guest/registered status, first/last name, and other profile data.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} args - The arguments containing user profile details (isGuest, firstName, lastName, etc.).
|
|
47
|
+
* @returns {object} - The user details object for identity events.
|
|
48
|
+
*/
|
|
48
49
|
_constructUserDetails(args) {
|
|
49
50
|
return {
|
|
50
51
|
isAnonymous: args.isGuest,
|
|
@@ -106,50 +107,138 @@ export class DataCloudApi {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Concatenates multiple event objects into a single object by merging their properties.
|
|
112
|
+
* Later objects in the argument list will override properties from earlier ones if there are conflicts.
|
|
113
|
+
*
|
|
114
|
+
* @param {...object} events - One or more event objects to be merged.
|
|
115
|
+
* @returns {object} - The merged event object containing all properties from the input objects.
|
|
116
|
+
*/
|
|
109
117
|
_concatenateEvents = (...events) => ({...events.reduce((acc, obj) => ({...acc, ...obj}), {})})
|
|
110
118
|
|
|
111
119
|
/**
|
|
112
|
-
*
|
|
120
|
+
* Constructs the party identification event object for a user.
|
|
121
|
+
* This includes identifiers and metadata for the user, such as guest or registered customer IDs.
|
|
113
122
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* @param {string} path - The URL path of the page that was viewed
|
|
117
|
-
* @param {object} args - Additional metadata for the event
|
|
123
|
+
* @param {object} args - The arguments containing user identification details (isGuest, guestId, customerId).
|
|
124
|
+
* @returns {object} - The party identification event object with required fields for the schema.
|
|
118
125
|
*/
|
|
119
|
-
|
|
126
|
+
_constructPartyIdentification(args) {
|
|
127
|
+
const partyIdentifier = args.isGuest ? args.guestId : args.customerId
|
|
128
|
+
const partyIdType = args.isGuest ? 'CC_USID' : 'CC_REGISTERED_CUSTOMER_ID'
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
party: partyIdentifier,
|
|
132
|
+
userId: partyIdentifier,
|
|
133
|
+
IDName: partyIdType,
|
|
134
|
+
IDType: partyIdType,
|
|
135
|
+
partyIdentificationId: partyIdentifier,
|
|
136
|
+
internalOrganizationId: this.siteId,
|
|
137
|
+
creationEventId: crypto.randomUUID()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Creates standard events (identity, party identification, contact point email)
|
|
143
|
+
* that are common across all send methods
|
|
144
|
+
*/
|
|
145
|
+
_createStandardEvents(args, additionalIdentityProps = {}) {
|
|
120
146
|
const baseEvent = this._constructBaseEvent(args)
|
|
121
147
|
const userDetails = this._constructUserDetails(args)
|
|
122
148
|
|
|
123
|
-
// If DNT, we do not send the identity Profile event
|
|
124
149
|
const identityProfile = this.dnt
|
|
125
150
|
? {}
|
|
126
151
|
: this._concatenateEvents(
|
|
127
152
|
baseEvent,
|
|
128
153
|
this._generateEventDetails('identity', 'Profile'),
|
|
129
154
|
userDetails,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
additionalIdentityProps
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const partyIdentification = this.dnt
|
|
159
|
+
? {}
|
|
160
|
+
: this._concatenateEvents(
|
|
161
|
+
baseEvent,
|
|
162
|
+
this._generateEventDetails('partyIdentification', 'Profile'),
|
|
163
|
+
this._constructPartyIdentification(args)
|
|
133
164
|
)
|
|
134
165
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
)
|
|
166
|
+
const contactPointEmail = args.email
|
|
167
|
+
? this._concatenateEvents(
|
|
168
|
+
baseEvent,
|
|
169
|
+
this._generateEventDetails('contactPointEmail', 'Profile', args.email)
|
|
170
|
+
)
|
|
171
|
+
: null
|
|
143
172
|
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
return {baseEvent, identityProfile, partyIdentification, contactPointEmail}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_handleApiError(err) {
|
|
177
|
+
if (err?.response?.status === 400) {
|
|
178
|
+
logger.warn(
|
|
179
|
+
'[DataCloudApi] 400 Bad Request: Check your Data Cloud configuration (appSourceId, tenantId) and event payload.',
|
|
180
|
+
{
|
|
181
|
+
namespace: 'use-datacloud._handleApiError',
|
|
182
|
+
additionalProperties: {error: err?.response}
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
} else {
|
|
186
|
+
logger.error('[DataCloudApi] Error sending Data Cloud event', {
|
|
187
|
+
namespace: 'use-datacloud._handleApiError',
|
|
188
|
+
additionalProperties: {error: err?.response}
|
|
189
|
+
})
|
|
146
190
|
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Constructs the interaction object and sends it to Data Cloud
|
|
195
|
+
*/
|
|
196
|
+
_sendInteraction(standardEvents, specificEvents) {
|
|
197
|
+
const {identityProfile, partyIdentification, contactPointEmail} = standardEvents
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
199
|
+
const interaction = {
|
|
200
|
+
events: [
|
|
201
|
+
...(!this.dnt ? [identityProfile, partyIdentification] : []),
|
|
202
|
+
...(contactPointEmail ? [contactPointEmail] : []),
|
|
203
|
+
...specificEvents
|
|
204
|
+
]
|
|
152
205
|
}
|
|
206
|
+
|
|
207
|
+
return this.sdk
|
|
208
|
+
.webEventsAppSourceIdPost(interaction)
|
|
209
|
+
.catch((err) => this._handleApiError(err))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Maps search results to DataCloud product format
|
|
214
|
+
*/
|
|
215
|
+
_mapSearchResultsToProducts(searchResults) {
|
|
216
|
+
return searchResults?.hits?.map((product) => this._constructDatacloudProduct(product)) || []
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Sends a `page-view` event to Data Cloud.
|
|
221
|
+
*
|
|
222
|
+
* This method records an `userEnagement` event type to track which page the shopper has viewed.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} path - The URL path of the page that was viewed
|
|
225
|
+
* @param {object} args - Additional metadata for the event
|
|
226
|
+
*/
|
|
227
|
+
async sendViewPage(path, args) {
|
|
228
|
+
const standardEvents = this._createStandardEvents(args, {sourceUrl: path})
|
|
229
|
+
|
|
230
|
+
const specificEvents = [
|
|
231
|
+
this._concatenateEvents(
|
|
232
|
+
standardEvents.baseEvent,
|
|
233
|
+
this._generateEventDetails('userEngagement', 'Engagement'),
|
|
234
|
+
{
|
|
235
|
+
interactionName: 'page-view',
|
|
236
|
+
sourceUrl: path
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
return this._sendInteraction(standardEvents, specificEvents)
|
|
153
242
|
}
|
|
154
243
|
|
|
155
244
|
/**
|
|
@@ -162,50 +251,22 @@ export class DataCloudApi {
|
|
|
162
251
|
* @param {object} args - Additional metadata for the event
|
|
163
252
|
*/
|
|
164
253
|
async sendViewProduct(product, args) {
|
|
165
|
-
const
|
|
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
|
-
)
|
|
254
|
+
const standardEvents = this._createStandardEvents(args)
|
|
176
255
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.
|
|
256
|
+
const specificEvents = [
|
|
257
|
+
this._concatenateEvents(
|
|
258
|
+
standardEvents.baseEvent,
|
|
259
|
+
this._generateEventDetails('catalog', 'Engagement'),
|
|
260
|
+
this._constructDatacloudProduct(product),
|
|
261
|
+
{
|
|
262
|
+
type: 'Product',
|
|
263
|
+
webStoreId: 'pwa',
|
|
264
|
+
interactionName: 'catalog-object-view-start'
|
|
265
|
+
}
|
|
182
266
|
)
|
|
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
|
-
}
|
|
267
|
+
]
|
|
203
268
|
|
|
204
|
-
|
|
205
|
-
this.sdk.webEventsAppSourceIdPost(interaction)
|
|
206
|
-
} catch (err) {
|
|
207
|
-
logger.error('Error sending DataCloud event', err)
|
|
208
|
-
}
|
|
269
|
+
return this._sendInteraction(standardEvents, specificEvents)
|
|
209
270
|
}
|
|
210
271
|
|
|
211
272
|
/**
|
|
@@ -222,16 +283,11 @@ export class DataCloudApi {
|
|
|
222
283
|
* @param {object} args - Additional metadata for the event
|
|
223
284
|
*/
|
|
224
285
|
async sendViewCategory(searchParams, category, searchResults, args) {
|
|
225
|
-
const
|
|
226
|
-
const userDetails = this._constructUserDetails(args)
|
|
286
|
+
const standardEvents = this._createStandardEvents(args)
|
|
227
287
|
|
|
228
|
-
const
|
|
229
|
-
this._constructDatacloudProduct(product)
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
const catalogObjects = products.map((product) => {
|
|
288
|
+
const specificEvents = this._mapSearchResultsToProducts(searchResults).map((product) => {
|
|
233
289
|
return this._concatenateEvents(
|
|
234
|
-
baseEvent,
|
|
290
|
+
standardEvents.baseEvent,
|
|
235
291
|
this._generateEventDetails('catalog', 'Engagement'),
|
|
236
292
|
this._constructBaseSearchResult(searchParams),
|
|
237
293
|
{
|
|
@@ -244,35 +300,7 @@ export class DataCloudApi {
|
|
|
244
300
|
)
|
|
245
301
|
})
|
|
246
302
|
|
|
247
|
-
|
|
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
|
-
}
|
|
303
|
+
return this._sendInteraction(standardEvents, specificEvents)
|
|
276
304
|
}
|
|
277
305
|
|
|
278
306
|
/**
|
|
@@ -289,16 +317,11 @@ export class DataCloudApi {
|
|
|
289
317
|
* @param {object} args - Additional metadata for the event
|
|
290
318
|
*/
|
|
291
319
|
async sendViewSearchResults(searchParams, searchResults, args) {
|
|
292
|
-
const
|
|
293
|
-
const userDetails = this._constructUserDetails(args)
|
|
294
|
-
|
|
295
|
-
const products = searchResults?.hits?.map((product) =>
|
|
296
|
-
this._constructDatacloudProduct(product)
|
|
297
|
-
)
|
|
320
|
+
const standardEvents = this._createStandardEvents(args)
|
|
298
321
|
|
|
299
|
-
const
|
|
322
|
+
const specificEvents = this._mapSearchResultsToProducts(searchResults).map((product) => {
|
|
300
323
|
return this._concatenateEvents(
|
|
301
|
-
baseEvent,
|
|
324
|
+
standardEvents.baseEvent,
|
|
302
325
|
this._generateEventDetails('catalog', 'Engagement'),
|
|
303
326
|
this._constructBaseSearchResult(searchParams),
|
|
304
327
|
{
|
|
@@ -311,35 +334,7 @@ export class DataCloudApi {
|
|
|
311
334
|
)
|
|
312
335
|
})
|
|
313
336
|
|
|
314
|
-
|
|
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
|
-
}
|
|
337
|
+
return this._sendInteraction(standardEvents, specificEvents)
|
|
343
338
|
}
|
|
344
339
|
|
|
345
340
|
/**
|
|
@@ -354,12 +349,11 @@ export class DataCloudApi {
|
|
|
354
349
|
* @param {object} args - Additional metadata for the event
|
|
355
350
|
*/
|
|
356
351
|
async sendViewRecommendations(recommenderDetails, products, args) {
|
|
357
|
-
const
|
|
358
|
-
const userDetails = this._constructUserDetails(args)
|
|
352
|
+
const standardEvents = this._createStandardEvents(args)
|
|
359
353
|
|
|
360
|
-
const
|
|
354
|
+
const specificEvents = products.map((product) => {
|
|
361
355
|
return this._concatenateEvents(
|
|
362
|
-
baseEvent,
|
|
356
|
+
standardEvents.baseEvent,
|
|
363
357
|
this._generateEventDetails('catalog', 'Engagement'),
|
|
364
358
|
{
|
|
365
359
|
id: product.id,
|
|
@@ -372,35 +366,7 @@ export class DataCloudApi {
|
|
|
372
366
|
)
|
|
373
367
|
})
|
|
374
368
|
|
|
375
|
-
|
|
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
|
-
}
|
|
369
|
+
return this._sendInteraction(standardEvents, specificEvents)
|
|
404
370
|
}
|
|
405
371
|
}
|
|
406
372
|
|
|
@@ -428,7 +394,7 @@ const useDataCloud = () => {
|
|
|
428
394
|
customerId: effectiveDnt ? '__DNT__' : customer?.customerId,
|
|
429
395
|
customerNo: effectiveDnt ? '__DNT__' : customer?.customerNo,
|
|
430
396
|
guestId: effectiveDnt ? '__DNT__' : usid,
|
|
431
|
-
deviceId: effectiveDnt ? '__DNT__' : usid,
|
|
397
|
+
deviceId: effectiveDnt ? '__DNT__' : customer?.customerId || usid,
|
|
432
398
|
sessionId: effectiveDnt ? '__DNT__' : sessionId,
|
|
433
399
|
firstName: customer?.firstName,
|
|
434
400
|
lastName: customer?.lastName,
|
|
@@ -436,23 +402,34 @@ const useDataCloud = () => {
|
|
|
436
402
|
}
|
|
437
403
|
}
|
|
438
404
|
|
|
439
|
-
// Grab Data Cloud configuration values
|
|
405
|
+
// Grab Data Cloud configuration values. Only initialize the SDK if config is present.
|
|
440
406
|
const {
|
|
441
|
-
app: {dataCloudAPI: config}
|
|
407
|
+
app: {dataCloudAPI: config = {}}
|
|
442
408
|
} = getConfig()
|
|
443
409
|
|
|
444
410
|
const {appSourceId, tenantId} = config
|
|
445
411
|
|
|
446
|
-
const dataCloud = useMemo(
|
|
447
|
-
()
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
412
|
+
const dataCloud = useMemo(() => {
|
|
413
|
+
if (!appSourceId || !tenantId) return null
|
|
414
|
+
return new DataCloudApi({
|
|
415
|
+
siteId: site.id,
|
|
416
|
+
appSourceId,
|
|
417
|
+
tenantId,
|
|
418
|
+
dnt: effectiveDnt
|
|
419
|
+
})
|
|
420
|
+
}, [site, appSourceId, tenantId, effectiveDnt])
|
|
421
|
+
|
|
422
|
+
// If Data Cloud config is missing, return no-op async functions for all event methods (SDK will not be initialized)
|
|
423
|
+
if (!appSourceId || !tenantId) {
|
|
424
|
+
const noop = async () => {}
|
|
425
|
+
return {
|
|
426
|
+
sendViewPage: noop,
|
|
427
|
+
sendViewProduct: noop,
|
|
428
|
+
sendViewCategory: noop,
|
|
429
|
+
sendViewSearchResults: noop,
|
|
430
|
+
sendViewRecommendations: noop
|
|
431
|
+
}
|
|
432
|
+
}
|
|
456
433
|
|
|
457
434
|
return {
|
|
458
435
|
async sendViewPage(...args) {
|