@okendo/shopify-hydrogen 2.4.0 → 2.5.1

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/README.md CHANGED
@@ -1,15 +1,15 @@
1
- > Note: this package is to be used on stores built with Hydrogen v2, based on Remix. If your store is built with the deprecated Hydrogen v1, please use the [version 1](https://www.npmjs.com/package/@okendo/shopify-hydrogen/v/1.6.6) of this package.
1
+ > **Note**: this package is to be used on stores built with **Shopify Hydrogen v2**. If your store is built with the deprecated Shopify Hydrogen v1, please use the [version 1](https://www.npmjs.com/package/@okendo/shopify-hydrogen/v/1.6.6) of this package.
2
2
 
3
- # Okendo Hydrogen 2 (Remix) React Components
3
+ > **Note**: the new version of Shopify Hydrogen v2 uses **React Router**. Previous versions used **Remix**. If your store is built with Remix, please use [version `2.4`](https://www.npmjs.com/package/@okendo/shopify-hydrogen/v/2.4.0) of this package.
4
4
 
5
- This package brings [Okendo's review widgets](https://www.okendo.io/blog/widget-plus/) and [Loyalty widgets](https://okendo.io/loyalty/) to a Shopify Hydrogen store.
5
+ # Okendo Hydrogen (React Router) React Components
6
+
7
+ This package brings Okendo's [Reviews widgets](https://okendo.io/reviews) and [Loyalty widgets](https://okendo.io/loyalty) to a Shopify Hydrogen store.
6
8
 
7
9
  ## Requirements
8
10
 
9
- - A Shopify store with the [**Okendo: Product Reviews & UCG**](https://apps.shopify.com/okendo-reviews) app installed and configured.
10
- - For existing merchants, your store must be upgraded to Okendo's Widget Plus widgets. It is free to upgrade. For more information please [contact Okendo Support](mailto:support@okendo.io).
11
- - A current Okendo subscription.
12
- - A [Shopify Hydrogen](https://hydrogen.shopify.dev/) app.
11
+ - A Shopify store with a [Hydrogen](https://hydrogen.shopify.dev/) storefront and [Okendo](https://apps.shopify.com/okendo-reviews) installed and configured.
12
+ - For existing merchants, your store must be using Okendo's Widget Plus widgets. [Contact us](mailto:support@okendo.io) if it's not the case, it's free to upgrade.
13
13
 
14
14
  ## Demo Store
15
15
 
@@ -17,418 +17,17 @@ Our demo store, which is based on the demo store provided by Shopify, can be fou
17
17
 
18
18
  > Note: there have been multiple versions of Shopify's Hydrogen demo store. If your project is based on an old version of it, consult the [history of our demo store's repository](https://github.com/okendo/okendo-shopify-hydrogen-demo/commits/master/).
19
19
 
20
- ## Exposition of Shopify Metafields <a id="expose-shopify-metafields" name="expose-shopify-metafields"></a>
21
-
22
- Okendo Reviews use Product and Shop [metafields](https://help.shopify.com/en/manual/custom-data/metafields). You will need to expose these metafields so that they can be retrieved by your Hydrogen app.
23
-
24
- At the moment, Shopify does not have a way of exposing Shop Metafields through their admin UI, so the preferred method is to [contact Okendo's Support](mailto:support@okendo.io).
25
-
26
- <details>
27
-
28
- <summary>If you're a technical user however, you can click here and follow the method to expose the metafields via the storefront API.</summary>
29
-
30
- ### Exposing Metafields via GraphQL
31
-
32
- You will need a **Storefront access token** with the following API access scopes:
33
-
34
- ```
35
- unauthenticated_read_content
36
- unauthenticated_read_customers
37
- unauthenticated_read_product_listings
38
- unauthenticated_read_product_inventory
39
- unauthenticated_read_product_pickup_locations
40
- unauthenticated_read_product_tags
41
- ```
42
-
43
- Follow the instructions on [this page](https://help.shopify.com/en/manual/apps/app-types/custom-apps#create-and-install-a-custom-app) to create it.
44
-
45
- #### Using Curl
46
-
47
- > Note: The settings metafields exist within the Okendo App's Reserved Namespace. To access these metafields we need to include the Okendo App ID (`app--1576377`) as a prefix in the `namespace` field.
48
-
49
- Open a new terminal or PowerShell window, then:
50
-
51
- 1. Run the following command to expose the `widget_pre_render_style_tags` shop metafield:
52
-
53
- ```bash
54
- curl -X POST \
55
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
56
- -H 'Content-Type: application/graphql' \
57
- -H 'X-Shopify-Access-Token: {access_token}' \
58
- -d '
59
- mutation {
60
- metafieldDefinitionCreate(
61
- definition: {
62
- name: "WidgetPreRenderStyleTags"
63
- namespace: "app--1576377--reviews"
64
- key: "widget_pre_render_style_tags"
65
- type: "multi_line_text_field"
66
- ownerType: SHOP
67
- access: {
68
- admin: PUBLIC_READ
69
- storefront: PUBLIC_READ
70
- }
71
- }
72
- ) {
73
- createdDefinition { id name }
74
- userErrors { field message code }
75
- }
76
- }
77
- '
78
- ```
79
-
80
- 2. Run the following command to expose the `widget_pre_render_body_style_tags` shop metafield:
81
-
82
- ```bash
83
- curl -X POST \
84
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
85
- -H 'Content-Type: application/graphql' \
86
- -H 'X-Shopify-Access-Token: {access_token}' \
87
- -d '
88
- mutation {
89
- metafieldDefinitionCreate(
90
- definition: {
91
- name: "WidgetPreRenderBodyStyleTags"
92
- namespace: "app--1576377--reviews"
93
- key: "widget_pre_render_body_style_tags"
94
- type: "multi_line_text_field"
95
- ownerType: SHOP
96
- access: {
97
- admin: PUBLIC_READ
98
- storefront: PUBLIC_READ
99
- }
100
- }
101
- ) {
102
- createdDefinition { id name }
103
- userErrors { field message code }
104
- }
105
- }
106
- '
107
- ```
108
-
109
- 3. Run the following command to expose the `reviews_widget_snippet` product metafield:
110
-
111
- ```bash
112
- curl -X POST \
113
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
114
- -H 'Content-Type: application/graphql' \
115
- -H 'X-Shopify-Access-Token: {access_token}' \
116
- -d '
117
- mutation {
118
- metafieldDefinitionCreate(
119
- definition: {
120
- name: "ReviewsWidgetSnippet"
121
- namespace: "app--1576377--reviews"
122
- key: "reviews_widget_snippet"
123
- type: "multi_line_text_field"
124
- ownerType: PRODUCT
125
- access: {
126
- admin: PUBLIC_READ
127
- storefront: PUBLIC_READ
128
- }
129
- }
130
- ) {
131
- createdDefinition { id name }
132
- userErrors { field message code }
133
- }
134
- }
135
- '
136
- ```
137
-
138
- 4. Run the following command to expose the `star_rating_snippet` product metafield:
139
-
140
- ```bash
141
- curl -X POST \
142
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
143
- -H 'Content-Type: application/graphql' \
144
- -H 'X-Shopify-Access-Token: {access_token}' \
145
- -d '
146
- mutation {
147
- metafieldDefinitionCreate(
148
- definition: {
149
- name: "StarRatingSnippet"
150
- namespace: "app--1576377--reviews"
151
- key: "star_rating_snippet"
152
- type: "multi_line_text_field"
153
- ownerType: PRODUCT
154
- access: {
155
- admin: PUBLIC_READ
156
- storefront: PUBLIC_READ
157
- }
158
- }
159
- ) {
160
- createdDefinition { id name }
161
- userErrors { field message code }
162
- }
163
- }
164
- '
165
- ```
166
-
167
- 5. Run the following command to expose the `review_count` product metafield:
168
-
169
- ```bash
170
- curl -X POST \
171
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
172
- -H 'Content-Type: application/graphql' \
173
- -H 'X-Shopify-Access-Token: {access_token}' \
174
- -d '
175
- mutation {
176
- metafieldDefinitionCreate(
177
- definition: {
178
- name: "ReviewCount"
179
- namespace: "app--1576377--reviews"
180
- key: "review_count"
181
- type: "number_integer"
182
- ownerType: PRODUCT
183
- access: {
184
- admin: PUBLIC_READ
185
- storefront: PUBLIC_READ
186
- }
187
- }
188
- ) {
189
- createdDefinition { id name }
190
- userErrors { field message code }
191
- }
192
- }
193
- '
194
- ```
195
-
196
- 6. Run the following command to expose the `average_rating` product metafield:
197
-
198
- ```bash
199
- curl -X POST \
200
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
201
- -H 'Content-Type: application/graphql' \
202
- -H 'X-Shopify-Access-Token: {access_token}' \
203
- -d '
204
- mutation {
205
- metafieldDefinitionCreate(
206
- definition: {
207
- name: "AverageRating"
208
- namespace: "app--1576377--reviews"
209
- key: "average_rating"
210
- type: "rating"
211
- ownerType: PRODUCT
212
- access: {
213
- admin: PUBLIC_READ
214
- storefront: PUBLIC_READ
215
- }
216
- }
217
- ) {
218
- createdDefinition { id name }
219
- userErrors { field message code }
220
- }
221
- }
222
- '
223
- ```
224
-
225
- ### Using GraphQL IDE
226
-
227
- Open your GraphQL IDE (such as Postman) and make `POST` requests with the following details:
228
-
229
- - **URL:** https://{shop}.myshopify.com/admin/api/2024-10/graphql.json
230
- - **Headers:** - X-Shopify-Access-Token: {access_token} - Content-Type: application/json
231
-
232
- 1. Execute the following request to expose the `widget_pre_render_style_tags` shop metafield:
233
-
234
- ```graphql
235
- mutation {
236
- metafieldDefinitionCreate(
237
- definition: {
238
- name: "WidgetPreRenderStyleTags"
239
- namespace: "app--1576377--reviews"
240
- key: "widget_pre_render_style_tags"
241
- type: "multi_line_text_field"
242
- ownerType: SHOP
243
- access: {
244
- admin: PUBLIC_READ,
245
- storefront: PUBLIC_READ
246
- }
247
- }
248
- ) {
249
- createdDefinition {
250
- id
251
- name
252
- }
253
- userErrors {
254
- field
255
- message
256
- code
257
- }
258
- }
259
- }
260
- ```
261
-
262
- 2. Execute the following request to expose the `widget_pre_render_body_style_tags` shop metafield:
263
-
264
- ```graphql
265
- mutation {
266
- metafieldDefinitionCreate(
267
- definition: {
268
- name: "WidgetPreRenderBodyStyleTags"
269
- namespace: "app--1576377--reviews"
270
- key: "widget_pre_render_body_style_tags"
271
- type: "multi_line_text_field"
272
- ownerType: SHOP
273
- access: {
274
- admin: PUBLIC_READ,
275
- storefront: PUBLIC_READ
276
- }
277
- }
278
- ) {
279
- createdDefinition {
280
- id
281
- name
282
- }
283
- userErrors {
284
- field
285
- message
286
- code
287
- }
288
- }
289
- }
290
- ```
291
-
292
- 3. Execute the following request to expose the `reviews_widget_snippet` product metafield:
293
-
294
- ```graphql
295
- mutation {
296
- metafieldDefinitionCreate(
297
- definition: {
298
- name: "ReviewsWidgetSnippet"
299
- namespace: "app--1576377--reviews"
300
- key: "reviews_widget_snippet"
301
- type: "multi_line_text_field"
302
- ownerType: PRODUCT
303
- access: {
304
- admin: PUBLIC_READ,
305
- storefront: PUBLIC_READ
306
- }
307
- }
308
- ) {
309
- createdDefinition {
310
- id
311
- name
312
- }
313
- userErrors {
314
- field
315
- message
316
- code
317
- }
318
- }
319
- }
320
- ```
321
-
322
- 4. Execute the following request to expose the `star_rating_snippet` product metafield:
323
-
324
- ```graphql
325
- mutation {
326
- metafieldDefinitionCreate(
327
- definition: {
328
- name: "StarRatingSnippet"
329
- namespace: "app--1576377--reviews"
330
- key: "star_rating_snippet"
331
- type: "multi_line_text_field"
332
- ownerType: PRODUCT
333
- access: {
334
- admin: PUBLIC_READ,
335
- storefront: PUBLIC_READ
336
- }
337
- }
338
- ) {
339
- createdDefinition {
340
- id
341
- name
342
- }
343
- userErrors {
344
- field
345
- message
346
- code
347
- }
348
- }
349
- }
350
- ```
351
-
352
- 5. Execute the following request to expose the `review_count` product metafield:
353
-
354
- ```graphql
355
- mutation {
356
- metafieldDefinitionCreate(
357
- definition: {
358
- name: "ReviewCount"
359
- namespace: "app--1576377--reviews"
360
- key: "review_count"
361
- type: "number_integer"
362
- ownerType: PRODUCT
363
- access: {
364
- admin: PUBLIC_READ,
365
- storefront: PUBLIC_READ
366
- }
367
- }
368
- ) {
369
- createdDefinition {
370
- id
371
- name
372
- }
373
- userErrors {
374
- field
375
- message
376
- code
377
- }
378
- }
379
- }
380
- ```
381
-
382
- 6. Execute the following request to expose the `average_rating` product metafield:
383
-
384
- ```graphql
385
- mutation {
386
- metafieldDefinitionCreate(
387
- definition: {
388
- name: "AverageRating"
389
- namespace: "app--1576377--reviews"
390
- key: "average_rating"
391
- type: "rating"
392
- ownerType: PRODUCT
393
- access: {
394
- admin: PUBLIC_READ,
395
- storefront: PUBLIC_READ
396
- }
397
- }
398
- ) {
399
- createdDefinition {
400
- id
401
- name
402
- }
403
- userErrors {
404
- field
405
- message
406
- code
407
- }
408
- }
409
- }
410
- ```
411
-
412
- **References**
413
-
414
- - [https://shopify.dev/api/examples/metafields#step-1-expose-metafields](https://shopify.dev/api/examples/metafields#step-1-expose-metafields)
415
- - [https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate](https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate)
416
-
417
- </details>
418
-
419
20
  ## Installation
420
21
 
421
22
  This package provides:
422
23
 
423
24
  - one function: `getOkendoProviderData`,
424
25
  - one provider: `OkendoProvider`,
425
- - two React components: `OkendoStarRating` and `OkendoReviews`.
26
+ - three React components: `OkendoStarRating`, `OkendoReviews`, and `OkendoReviewsCarousel`.
426
27
 
427
- The function `getOkendoProviderData` needs to be called in the `loader` function of `root.tsx` in the Hydrogen 2 store. The data is then passed to `OkendoProvider`, which is added to your website's `body` and wraps everything in it.
28
+ The function `getOkendoProviderData` needs to be called in the `loader` function of `root.tsx` in your Hydrogen store. The data is then passed to `OkendoProvider`, which is added to your website's `body` and wraps everything in it.
428
29
 
429
- > Important: `OkendoProvider` supports two ways of loading the data returned by `getOkendoProviderData`: either as a promise, or as the data itself. Its behaviour is different between the two ways this is explained below.
430
-
431
- Then, the components `OkendoStarRating` and `OkendoReviews` can be added on the store pages. There are a few more bits of configuration to do, please see below.
30
+ Then, the React components can be added on your store pages. There are a few more bits of configuration to do, please see below.
432
31
 
433
32
  > The code examples provided in this section are based on the Shopify template store created by running `npm create @shopify/hydrogen@latest` (see [Shopify's documentation](https://shopify.dev/docs/custom-storefronts/hydrogen/getting-started)). You will find the following steps already done in [our demo store](https://github.com/okendo/okendo-shopify-hydrogen-demo).
434
33
 
@@ -440,64 +39,42 @@ npm i @okendo/shopify-hydrogen
440
39
 
441
40
  ### `app/root.tsx`
442
41
 
443
- `OkendoProvider` supports two ways of loading the data returned by `getOkendoProviderData`:
444
-
445
- - **as a promise**: in this case, the query getting the data is deferred, which allows your page to load as quickly as it does without Okendo's widgets. When the data is ready, it is sent to the browser, and Okendo's widgets are rendered. Blank placeholders are shown until the widgets are rendered. You can customise these placeholders to show loading spinners or skeletons that fit well with your store's theme.
446
- - **as the data**: in this case, the query getting the data needs to complete before your page loads, which can add a couple hundreds milliseconds of loading time. Widgets are then rendered server-side, and so appear as soon as your page loads.
447
-
448
- To summarise the differences between the two behaviours:
449
-
450
- - Pass the promise to `OkendoProvider` — so don't use `await` with `getOkendoProviderData`:
451
-
452
- - The page loading time won't be increased at all.
453
- - The widgets will be rendered client-side.
454
- - Placeholders (which are customisable) are shown until the widgets are rendered.
455
-
456
- - Pass the data to `OkendoProvider` — so use `await` with `getOkendoProviderData`:
457
- - The page loading time can be increased by a couple hundreds milliseconds.
458
- - The widgets will be rendered server-side.
459
- - The widgets are shown as soon as the page loads — no placeholders needed.
460
-
461
- You can easily experiment with the two ways, and decide which is the one you'd like to keep for your store.
462
-
463
42
  Open `app/root.tsx` and add the following import:
464
43
 
465
44
  ```ts
466
45
  import {
467
- OkendoProvider,
468
- getOkendoProviderData,
46
+ OkendoProvider,
47
+ getOkendoProviderData,
469
48
  } from '@okendo/shopify-hydrogen';
470
49
  ```
471
50
 
472
- Locate the `loader` function, append `okendoProviderData` to the returned data as shown below, and set `subscriberId` to your Okendo subscriber ID.
473
-
474
- As explained above, set `okendoProviderData` to either `getOkendoProviderData(...)`, or `await getOkendoProviderData(...)`:
51
+ Locate the `loadDeferredData` function, append `okendoProviderData` to the returned data as shown below, and set `subscriberId` to your Okendo subscriber ID.
475
52
 
476
53
  ```ts
477
- return defer({
478
- // ...
479
- okendoProviderData:
480
- /* place `await` here if you want server-rendered widgets */ getOkendoProviderData(
481
- {
482
- context: args.context,
483
- subscriberId: '<your-okendo-subscriber-id>',
484
- },
485
- ),
486
- });
54
+ // ...
55
+ return {
56
+ cart: cart.get(),
57
+ isLoggedIn: customerAccount.isLoggedIn(),
58
+ footer,
59
+ okendoProviderData: getOkendoProviderData({
60
+ context,
61
+ subscriberId: '<your-okendo-subscriber-id>',
62
+ }),
63
+ };
487
64
  ```
488
65
 
489
66
  Locate the `Layout` component, add the `meta` tag `oke:subscriber_id` to `head`, and place your Okendo subscriber ID in its content:
490
67
 
491
68
  ```tsx
492
69
  <head>
493
- <meta charSet="utf-8" />
494
- <meta name="viewport" content="width=device-width,initial-scale=1" />
495
- <meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
496
- ...
70
+ <meta charSet="utf-8" />
71
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
72
+ <meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
73
+ ...
497
74
  </head>
498
75
  ```
499
76
 
500
- Append `OkendoProvider` to `body`, and pass it the promise — or the data — returned by `getOkendoProviderData`. If Content Security Policy is active in your project, you also need to provide the `nonce` (available with `const nonce = useNonce()` in Shopify's Hydrogen demo store):
77
+ Append `OkendoProvider` to `body`, and pass it the promise returned by `getOkendoProviderData`. If Content Security Policy is active in your project, you also need to provide the `nonce` (available with `const nonce = useNonce()` in Shopify's Hydrogen demo store):
501
78
 
502
79
  ```tsx
503
80
  <body>
@@ -507,10 +84,10 @@ Append `OkendoProvider` to `body`, and pass it the promise — or the data — r
507
84
  cart={data.cart}
508
85
  shop={data.shop}
509
86
  consent={data.consent}
510
- >
87
+ >
511
88
  <PageLayout {...data}>{children}</PageLayout>
512
89
  </Analytics.Provider>
513
- </OkendoProvider>
90
+ </OkendoProvider>
514
91
  ) : (
515
92
  children
516
93
  )}
@@ -524,7 +101,8 @@ Append `OkendoProvider` to `body`, and pass it the promise — or the data — r
524
101
  > This is only necessary if Content Security Policy is active in your project.
525
102
 
526
103
  Locate the call to `createContentSecurityPolicy`, and ensure your configuration includes the entries below:
527
- Note that it's necessary to to add the default values (`'self'`, etc.) when [extending the CSP](https://shopify.dev/docs/custom-storefronts/hydrogen/content-security-policy). The call to `createContentSecurityPolicy` should now look like the following:
104
+
105
+ > Note that it's necessary to to add the default values (`'self'`, etc.) when [extending the CSP](https://shopify.dev/docs/custom-storefronts/hydrogen/content-security-policy). The call to `createContentSecurityPolicy` should now look like the following:
528
106
 
529
107
  ```ts
530
108
  const { nonce, header, NonceProvider } = createContentSecurityPolicy({
@@ -532,8 +110,8 @@ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
532
110
  checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
533
111
  storeDomain: context.env.PUBLIC_STORE_DOMAIN,
534
112
  },
535
- defaultSrc: [
536
- "'self'",
113
+ defaultSrc: [
114
+ "'self'",
537
115
  'localhost:*',
538
116
  'https://cdn.shopify.com',
539
117
  'https://www.google.com',
@@ -545,9 +123,9 @@ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
545
123
  'https://surveys.okendo.io',
546
124
  'https://api.okendo.io',
547
125
  'data:',
548
- ],
549
- imgSrc: [
550
- "'self'",
126
+ ],
127
+ imgSrc: [
128
+ "'self'",
551
129
  'https://cdn.shopify.com',
552
130
  'data:',
553
131
  'https://d3hw6dc1ow8pp2.cloudfront.net',
@@ -555,26 +133,26 @@ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
555
133
  'https://dov7r31oq5dkj.cloudfront.net',
556
134
  'https://cdn-static.okendo.io',
557
135
  'https://surveys.okendo.io',
558
- ],
559
- mediaSrc: [
560
- "'self'",
136
+ ],
137
+ mediaSrc: [
138
+ "'self'",
561
139
  'https://d3hw6dc1ow8pp2.cloudfront.net',
562
140
  'https://d3g5hqndtiniji.cloudfront.net',
563
141
  'https://dov7r31oq5dkj.cloudfront.net',
564
142
  'https://cdn-static.okendo.io',
565
- ],
143
+ ],
566
144
  styleSrc: [
567
- "'self'",
568
- "'unsafe-inline'",
145
+ "'self'",
146
+ "'unsafe-inline'",
569
147
  'https://cdn.shopify.com',
570
148
  'https://fonts.googleapis.com',
571
149
  'https://fonts.gstatic.com',
572
150
  'https://d3hw6dc1ow8pp2.cloudfront.net',
573
151
  'https://cdn-static.okendo.io',
574
152
  'https://surveys.okendo.io',
575
- ],
576
- scriptSrc: [
577
- "'self'",
153
+ ],
154
+ scriptSrc: [
155
+ "'self'",
578
156
  'https://cdn.shopify.com',
579
157
  'https://d3hw6dc1ow8pp2.cloudfront.net',
580
158
  'https://dov7r31oq5dkj.cloudfront.net',
@@ -583,18 +161,18 @@ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
583
161
  'https://api.okendo.io',
584
162
  'https://www.google.com',
585
163
  'https://www.gstatic.com',
586
- ],
587
- fontSrc: [
588
- "'self'",
164
+ ],
165
+ fontSrc: [
166
+ "'self'",
589
167
  'https://fonts.gstatic.com',
590
168
  'https://d3hw6dc1ow8pp2.cloudfront.net',
591
169
  'https://dov7r31oq5dkj.cloudfront.net',
592
170
  'https://cdn.shopify.com',
593
171
  'https://cdn-static.okendo.io',
594
172
  'https://surveys.okendo.io',
595
- ],
596
- connectSrc: [
597
- "'self'",
173
+ ],
174
+ connectSrc: [
175
+ "'self'",
598
176
  'https://monorail-edge.shopifysvc.com',
599
177
  'localhost:*',
600
178
  'ws://localhost:*',
@@ -605,223 +183,313 @@ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
605
183
  'https://api.raygun.com',
606
184
  'https://www.google.com',
607
185
  'https://www.gstatic.com',
608
- ],
186
+ ],
609
187
  frameSrc: ['https://www.google.com', 'https://www.gstatic.com'],
610
188
  });
611
189
  ```
612
190
 
191
+ ### `app/lib/fragments.ts`
192
+
193
+ Add the following GraphQL fragment at the bottom of the file:
194
+
195
+ ```ts
196
+ export const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
197
+ fragment OkendoStarRatingSnippet on Product {
198
+ okendoStarRatingSnippet: metafield(
199
+ namespace: "app--1576377--reviews"
200
+ key: "star_rating_snippet"
201
+ ) {
202
+ value
203
+ }
204
+ }
205
+ ` as const;
206
+
207
+ export const OKENDO_PRODUCT_REVIEWS_FRAGMENT = `#graphql
208
+ fragment OkendoReviewsSnippet on Product {
209
+ okendoReviewsSnippet: metafield(
210
+ namespace: "app--1576377--reviews"
211
+ key: "reviews_widget_snippet"
212
+ ) {
213
+ value
214
+ }
215
+ }
216
+ ` as const;
217
+ ```
218
+
613
219
  ### `app/routes/_index.tsx`
614
220
 
615
221
  Add the following import:
616
222
 
617
223
  ```ts
618
- import { OkendoStarRating } from '@okendo/shopify-hydrogen';
224
+ import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from '~/lib/fragments';
225
+ ```
226
+
227
+ Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `RECOMMENDED_PRODUCTS_QUERY`:
228
+
229
+ ```ts
230
+ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
231
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
232
+ fragment RecommendedProduct on Product {
233
+ id
234
+ title
235
+ handle
236
+ priceRange {
237
+ minVariantPrice {
238
+ amount
239
+ currencyCode
240
+ }
241
+ }
242
+ images(first: 1) {
243
+ nodes {
244
+ id
245
+ url
246
+ altText
247
+ width
248
+ height
249
+ }
250
+ }
251
+ ...OkendoStarRatingSnippet
252
+ }
253
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
254
+ @inContext(country: $country, language: $language) {
255
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
256
+ nodes {
257
+ ...RecommendedProduct
258
+ }
259
+ }
260
+ }
261
+ ` as const;
262
+ ```
263
+
264
+ ### `app/routes/collections.all.tsx`
265
+
266
+ Add the following import:
267
+
268
+ ```ts
269
+ import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from '~/lib/fragments';
619
270
  ```
620
271
 
621
- Add the following block just before the `RECOMMENDED_PRODUCTS_QUERY` GraphQL query:
272
+ Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `COLLECTION_ITEM_FRAGMENT`:
622
273
 
623
274
  ```ts
624
- const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
625
- fragment OkendoStarRatingSnippet on Product {
626
- okendoStarRatingSnippet: metafield(
627
- namespace: "app--1576377--reviews"
628
- key: "star_rating_snippet"
629
- ) {
630
- value
631
- }
632
- }
275
+ const COLLECTION_ITEM_FRAGMENT = `#graphql
276
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
277
+ fragment MoneyCollectionItem on MoneyV2 {
278
+ amount
279
+ currencyCode
280
+ }
281
+ fragment CollectionItem on Product {
282
+ id
283
+ handle
284
+ title
285
+ featuredImage {
286
+ id
287
+ altText
288
+ url
289
+ width
290
+ height
291
+ }
292
+ priceRange {
293
+ minVariantPrice {
294
+ ...MoneyCollectionItem
295
+ }
296
+ maxVariantPrice {
297
+ ...MoneyCollectionItem
298
+ }
299
+ }
300
+ ...OkendoStarRatingSnippet
301
+ }
633
302
  ` as const;
634
303
  ```
635
304
 
636
- Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `RECOMMENDED_PRODUCTS_QUERY`:
305
+ ### `app/routes/collections.$handle.tsx`
306
+
307
+ Add the following import:
637
308
 
638
309
  ```ts
639
- const RECOMMENDED_PRODUCTS_QUERY = `#graphql
640
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
641
- fragment RecommendedProduct on Product {
642
- id
643
- title
644
- handle
645
- priceRange {
646
- minVariantPrice {
647
- amount
648
- currencyCode
649
- }
650
- }
651
- images(first: 1) {
652
- nodes {
653
- id
654
- url
655
- altText
656
- width
657
- height
658
- }
659
- }
660
- ...OkendoStarRatingSnippet
661
- }
662
- query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
663
- @inContext(country: $country, language: $language) {
664
- products(first: 4, sortKey: UPDATED_AT, reverse: true) {
665
- nodes {
666
- ...RecommendedProduct
667
- }
668
- }
669
- }
310
+ import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from '~/lib/fragments';
311
+ ```
312
+
313
+ Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `COLLECTION_ITEM_FRAGMENT`:
314
+
315
+ ```ts
316
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
317
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
318
+ fragment MoneyProductItem on MoneyV2 {
319
+ amount
320
+ currencyCode
321
+ }
322
+ fragment ProductItem on Product {
323
+ id
324
+ handle
325
+ title
326
+ featuredImage {
327
+ id
328
+ altText
329
+ url
330
+ width
331
+ height
332
+ }
333
+ priceRange {
334
+ minVariantPrice {
335
+ ...MoneyProductItem
336
+ }
337
+ maxVariantPrice {
338
+ ...MoneyProductItem
339
+ }
340
+ }
341
+ ...OkendoStarRatingSnippet
342
+ }
670
343
  ` as const;
671
344
  ```
672
345
 
673
- > Note: if you get a type error on `product`, restart the dev server to get the types (`storefrontapi.generated.d.ts`) regenerated from the GraphQL fragments.
346
+ ### `app/components/ProductItem.tsx`
347
+
348
+ Add the following import:
349
+
350
+ ```ts
351
+ import { OkendoStarRating } from '@okendo/shopify-hydrogen';
352
+ ```
674
353
 
675
354
  Add `OkendoStarRating` to the `RecommendedProducts` component — for instance, we can add it below the product title, like this:
676
355
 
677
356
  ```tsx
678
357
  <Image
679
- data={product.images.nodes[0]}
680
- aspectRatio="1/1"
681
- sizes="(min-width: 45em) 20vw, 50vw"
358
+ data={product.images.nodes[0]}
359
+ aspectRatio="1/1"
360
+ sizes="(min-width: 45em) 20vw, 50vw"
682
361
  />
683
362
  <h4>{product.title}</h4>
684
363
  <OkendoStarRating
685
- productId={product.id}
686
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
364
+ className="mb-2"
365
+ productId={product.id}
366
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
687
367
  />
688
368
  <small>
689
- <Money data={product.priceRange.minVariantPrice} />
369
+ <Money data={product.priceRange.minVariantPrice} />
690
370
  </small>
691
371
  ```
692
372
 
693
- > Note: if the widgets are rendered client-side (if you don't use `await` when calling `getOkendoProviderData`), you can provide your own placeholder by using the `placeholder` property of `OkendoStarRating`.
373
+ > Note: if you get a type error on `product`, restart the dev server to get the types (`storefrontapi.generated.d.ts`) regenerated from the GraphQL fragments.
694
374
 
695
375
  We now have the Okendo Star Rating widget visible on our page:
696
376
 
697
- ![Okendo's Star Rating widget](./readme-res/okendo-star-rating-widget.png)
698
-
699
- You can do the same changes to the files `app/routes/collections.$handle.tsx` and `app/routes/collections.all.tsx` to make the items' Okendo Star Rating visible on collection pages.
377
+ ![Okendo's Star Rating widget](./res/okendo-star-rating-widget.webp)
700
378
 
701
379
  ### `app/routes/products.$handle.tsx`
702
380
 
703
- Add the following import:
381
+ Add the following imports:
704
382
 
705
383
  ```ts
706
384
  import { OkendoReviews, OkendoStarRating } from '@okendo/shopify-hydrogen';
707
- ```
708
-
709
- Add the following block just before the `PRODUCT_FRAGMENT` GraphQL query:
710
-
711
- ```ts
712
- const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
713
- fragment OkendoStarRatingSnippet on Product {
714
- okendoStarRatingSnippet: metafield(
715
- namespace: "app--1576377--reviews"
716
- key: "star_rating_snippet"
717
- ) {
718
- value
719
- }
720
- }
721
- ` as const;
722
-
723
- const OKENDO_PRODUCT_REVIEWS_FRAGMENT = `#graphql
724
- fragment OkendoReviewsSnippet on Product {
725
- okendoReviewsSnippet: metafield(
726
- namespace: "app--1576377--reviews"
727
- key: "reviews_widget_snippet"
728
- ) {
729
- value
730
- }
731
- }
732
- ` as const;
385
+ import {
386
+ OKENDO_PRODUCT_REVIEWS_FRAGMENT,
387
+ OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
388
+ } from '~/lib/fragments';
733
389
  ```
734
390
 
735
391
  Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}`, `${OKENDO_PRODUCT_REVIEWS_FRAGMENT}`, `...OkendoStarRatingSnippet`, and `...OkendoReviewsSnippet` to `PRODUCT_FRAGMENT`:
736
392
 
737
393
  ```ts
738
394
  const PRODUCT_FRAGMENT = `#graphql
739
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
740
- ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
741
- fragment Product on Product {
742
- id
743
- title
744
- vendor
745
- handle
746
- descriptionHtml
747
- description
748
- options {
749
- name
750
- values
751
- }
752
- selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
753
- ...ProductVariant
754
- }
755
- variants(first: 1) {
756
- nodes {
757
- ...ProductVariant
758
- }
759
- }
760
- seo {
761
- description
762
- title
763
- }
764
- ...OkendoStarRatingSnippet
765
- ...OkendoReviewsSnippet
766
- }
767
- ${PRODUCT_VARIANT_FRAGMENT}
395
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
396
+ ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
397
+ fragment Product on Product {
398
+ id
399
+ title
400
+ vendor
401
+ handle
402
+ descriptionHtml
403
+ description
404
+ encodedVariantExistence
405
+ encodedVariantAvailability
406
+ options {
407
+ name
408
+ optionValues {
409
+ name
410
+ firstSelectableVariant {
411
+ ...ProductVariant
412
+ }
413
+ swatch {
414
+ color
415
+ image {
416
+ previewImage {
417
+ url
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }
423
+ selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
424
+ ...ProductVariant
425
+ }
426
+ adjacentVariants (selectedOptions: $selectedOptions) {
427
+ ...ProductVariant
428
+ }
429
+ seo {
430
+ description
431
+ title
432
+ }
433
+ ...OkendoStarRatingSnippet
434
+ ...OkendoReviewsSnippet
435
+ }
436
+ ${PRODUCT_VARIANT_FRAGMENT}
768
437
  ` as const;
769
438
  ```
770
439
 
771
- > Note: if you get a type error on `product`, restart the dev server to get the types (`storefrontapi.generated.d.ts`) regenerated from the GraphQL fragments.
772
-
773
440
  Add `OkendoStarRating` and `OkendoReviews` to the `Product` component:
774
441
 
775
442
  ```tsx
776
443
  <>
777
- <div className="product">
778
- <ProductImage image={selectedVariant?.image} />
444
+ <div className="product">
445
+ <ProductImage image={selectedVariant?.image} />
779
446
  <div className="product-main">
780
447
  <h1>{title}</h1>
781
448
  <OkendoStarRating
449
+ className="mb-4"
782
450
  productId={product.id}
783
451
  okendoStarRatingSnippet={product.okendoStarRatingSnippet}
784
- />
452
+ />
785
453
  <ProductPrice
786
454
  price={selectedVariant?.price}
787
455
  compareAtPrice={selectedVariant?.compareAtPrice}
788
456
  />
789
457
  ...
790
- </div>
458
+ </div>
791
459
  ...
792
460
  </div>
793
461
 
794
- <OkendoReviews
795
- productId={product.id}
796
- okendoReviewsSnippet={product.okendoReviewsSnippet}
797
- />
462
+ <OkendoReviews
463
+ productId={product.id}
464
+ okendoReviewsSnippet={product.okendoReviewsSnippet}
465
+ />
798
466
  </>
799
467
  ```
800
468
 
801
- > Note: if the widgets are rendered client-side (if you don't use `await` when calling `getOkendoProviderData`), you can provide your own placeholder by using the `placeholder` property of `OkendoStarRating` and `OkendoReviews`.
469
+ > Note: if you get a type error on `product`, restart the dev server to get the types (`storefrontapi.generated.d.ts`) regenerated from the GraphQL fragments.
802
470
 
803
471
  We now have the Okendo Star Rating and Reviews widgets visible on our product page:
804
472
 
805
- ![Okendo's Star Rating and Reviews widgets](./readme-res/okendo-star-rating-and-reviews-widgets.png)
473
+ ![Okendo's Star Rating and Reviews widgets](./res/okendo-star-rating-and-reviews-widgets.webp)
806
474
 
807
- ### All Reviews Widget - Client Side Only
475
+ ### All-Reviews Widget - Client Side Only
808
476
 
809
- If you would like to include a copy of the Okendo Reviews Widget which displays all reviews for a given store (to be used on a reviews page for example), please add the `OkendoReviewsWidget` without supplying the `productId`.
477
+ If you would like to include a copy of the Okendo Reviews Widget which displays all reviews for a given store (to be used on a reviews page for example), please add `OkendoReviews` without supplying the `productId`.
810
478
 
811
- Please note the all reviews widget loads on the client, not the server.
479
+ Please note the all-reviews widget loads on the client, not the server.
812
480
 
813
481
  ```tsx
814
- import { type MetaFunction } from '@remix-run/react';
482
+ import { type MetaFunction } from 'react-router';
815
483
  import { OkendoReviews } from '@okendo/shopify-hydrogen';
816
484
 
817
485
  export const meta: MetaFunction = () => {
818
- return [{title: `Hydrogen | Okendo All Reviews`}];
486
+ return [{ title: `Hydrogen | Okendo All Reviews` }];
819
487
  };
820
488
 
821
489
  export default function ReviewsPage() {
822
490
  return (
823
491
  <div className="all-reviews">
824
- <h1>All Reviews Widget</h1>
492
+ <h1>All-Reviews Widget</h1>
825
493
  <OkendoReviews />
826
494
  </div>
827
495
  );
@@ -829,32 +497,48 @@ export default function ReviewsPage() {
829
497
  ```
830
498
 
831
499
  ### Okendo Reviews Carousel Widget - Client Side Only
832
- If you would like to include a copy of the Okendo Reviews Carousel Widget which displays reviews by product or group for a given store (to be used on a homepage or featured page for example), please add the `OkendoReviewsCarouselWidget` with or without the the `productId` or `groupId`.
833
500
 
834
- Please note the all reviews widget loads on the client not the server.
501
+ If you would like to include a copy of the Okendo Reviews Carousel Widget which displays reviews by product or group for a given store (to be used on a homepage or featured page for example), please add `OkendoReviewsCarousel` with or without the `productId` or `groupId`.
502
+
503
+ Please note the carousel widget loads on the client, not the server.
835
504
 
836
505
  ```tsx
837
- import { type MetaFunction } from '@remix-run/react';
506
+ import { type MetaFunction } from 'react-router';
838
507
  import { OkendoReviews } from '@okendo/shopify-hydrogen';
839
508
 
840
509
  export const meta: MetaFunction = () => {
841
- return [{title: `Hydrogen | Okendo Reviews Carousel`}];
510
+ return [{ title: `Hydrogen | Okendo Reviews Carousel` }];
842
511
  };
843
512
 
844
513
  export default function AFeaturedPage() {
845
514
  return (
846
515
  <div className="all-reviews">
847
516
  <h1>Reviews Carousel Widget</h1>
848
- <OkendoReviewsCarousel
849
- productId={product.id}
850
- />
517
+ <OkendoReviewsCarousel productId={product.id} />
518
+ </div>
519
+ );
520
+ }
521
+ ```
522
+
523
+ You can also use `OkendoReviewsCarousel` without `productId`, in order to display reviews for all products. For instance, we can add it to the homepage in `app/routes/_index.tsx`:
524
+
525
+ ```ts
526
+ export default function Homepage() {
527
+ const data = useLoaderData<typeof loader>();
528
+ return (
529
+ <div className="home">
530
+ <FeaturedCollection collection={data.featuredCollection} />
531
+ <RecommendedProducts products={data.recommendedProducts} />
532
+ <OkendoReviewsCarousel />
851
533
  </div>
852
534
  );
853
535
  }
854
536
  ```
855
537
 
856
538
  # Loyalty Widgets
539
+
857
540
  ## Installation
541
+
858
542
  To include Loyalty Widgets in your Shopify Hydrogen store, you will need to make the following changes:
859
543
 
860
544
  1. Add `customerAccessToken: await args.context.customerAccount.getAccessToken(),` to your `loader` function, this will be used to log your customer into the Loyalty App.
@@ -864,35 +548,35 @@ To include Loyalty Widgets in your Shopify Hydrogen store, you will need to make
864
548
  > Note: If you only wish to use the Loyalty product and not reviews then simply leave out the `'reviews'` from the array like so: `okendoProducts: ['loyalty'],`.
865
549
 
866
550
  The relevant section should now look something like this:
551
+
867
552
  ```ts
868
553
  return defer({
869
554
  // ...
870
555
  customerAccessToken: await args.context.customerAccount.getAccessToken(),
871
- okendoProviderData:
872
- getOkendoProviderData(
873
- {
874
- context: args.context,
875
- subscriberId: '<your-okendo-subscriber-id>',
876
- okendoProducts: ['reviews', 'loyalty'],
877
- },
878
- ),
556
+ okendoProviderData: getOkendoProviderData({
557
+ context: args.context,
558
+ subscriberId: '<your-okendo-subscriber-id>',
559
+ okendoProducts: ['reviews', 'loyalty'],
560
+ }),
879
561
  });
880
562
  ```
881
563
 
882
564
  3. Add `customerAccessToken={data.customerAccessToken}` to the `OkendoProvider` component, it should now look like:
883
- ```ts
565
+
566
+ ```tsx
884
567
  <OkendoProvider
885
- nonce={nonce}
886
- okendoProviderData={data.okendoProviderData}
887
- customerAccessToken={data.customerAccessToken}
568
+ nonce={nonce}
569
+ okendoProviderData={data.okendoProviderData}
570
+ customerAccessToken={data.customerAccessToken}
888
571
  >
889
- ...
572
+ ...
890
573
  </OkendoProvider>
891
574
  ```
892
575
 
893
576
  If your Okendo Loyalty Settings are [correctly set up](https://support.okendo.io/en/collections/8270193-okendo-loyalty) and your program has launched, the Loyalty Floating Widget will now appear on your store.
894
577
 
895
578
  ## Dedicated Loyalty Page
579
+
896
580
  Add `<OkendoLoyaltyEmbeddedWidget />` to any components/pages where you wish to have the Dedicated Loyalty Page appear.
897
581
 
898
- *Make sure you are importing the component from the `okendo-shopify-hydrogen` package: `import {OkendoLoyaltyEmbeddedWidget} from '@okendo/shopify-hydrogen';`*
582
+ _Make sure you are importing the component from the `okendo-shopify-hydrogen` package: `import {OkendoLoyaltyEmbeddedWidget} from '@okendo/shopify-hydrogen';`_