@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,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
+ })