@okendo/shopify-hydrogen 2.2.6 → 2.2.7

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.
Files changed (3) hide show
  1. package/LICENSE.txt +3 -3
  2. package/README.md +870 -870
  3. package/package.json +48 -48
package/README.md CHANGED
@@ -1,871 +1,871 @@
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.3.0) of this package.
2
-
3
- # Okendo Hydrogen 2 (Remix) React Components
4
-
5
- This package brings [Okendo's review widgets](https://www.okendo.io/blog/widget-plus/) to a Shopify Hydrogen store.
6
-
7
- ## Requirements
8
-
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.
13
-
14
- ## Demo Store
15
-
16
- Our demo store, which is based on the demo store provided by Shopify, can be found [here](https://github.com/okendo/okendo-shopify-hydrogen-demo).
17
-
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.
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
-
46
- #### Using Curl
47
-
48
- Open a new terminal or PowerShell window, then:
49
-
50
- 1. Run the following command to expose the `widget_pre_render_style_tags` shop metafield:
51
-
52
- ```bash
53
- curl -X POST \
54
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
55
- -H 'Content-Type: application/graphql' \
56
- -H 'X-Shopify-Access-Token: {access_token}' \
57
- -d '
58
- mutation {
59
- metafieldDefinitionCreate(
60
- definition: {
61
- name: "WidgetPreRenderStyleTags"
62
- namespace: "$app:review"
63
- key: "widget_pre_render_style_tags"
64
- type: "multi_line_text_field"
65
- ownerType: SHOP
66
- access: {
67
- admin: PUBLIC_READ
68
- storefront: PUBLIC_READ
69
- }
70
- }
71
- ) {
72
- createdDefinition { id name }
73
- userErrors { field message code }
74
- }
75
- }
76
- '
77
- ```
78
-
79
- 2. Run the following command to expose the `widget_pre_render_body_style_tags` shop metafield:
80
-
81
- ```bash
82
- curl -X POST \
83
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
84
- -H 'Content-Type: application/graphql' \
85
- -H 'X-Shopify-Access-Token: {access_token}' \
86
- -d '
87
- mutation {
88
- metafieldDefinitionCreate(
89
- definition: {
90
- name: "WidgetPreRenderBodyStyleTags"
91
- namespace: "$app:review"
92
- key: "widget_pre_render_body_style_tags"
93
- type: "multi_line_text_field"
94
- ownerType: SHOP
95
- access: {
96
- admin: PUBLIC_READ
97
- storefront: PUBLIC_READ
98
- }
99
- }
100
- ) {
101
- createdDefinition { id name }
102
- userErrors { field message code }
103
- }
104
- }
105
- '
106
- ```
107
-
108
- 3. Run the following command to expose the `reviews_widget_snippet` product metafield:
109
-
110
- ```bash
111
- curl -X POST \
112
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
113
- -H 'Content-Type: application/graphql' \
114
- -H 'X-Shopify-Access-Token: {access_token}' \
115
- -d '
116
- mutation {
117
- metafieldDefinitionCreate(
118
- definition: {
119
- name: "ReviewsWidgetSnippet"
120
- namespace: "$app:reviews"
121
- key: "reviews_widget_snippet"
122
- type: "multi_line_text_field"
123
- ownerType: PRODUCT
124
- access: {
125
- admin: PUBLIC_READ
126
- storefront: PUBLIC_READ
127
- }
128
- }
129
- ) {
130
- createdDefinition { id name }
131
- userErrors { field message code }
132
- }
133
- }
134
- '
135
- ```
136
-
137
- 4. Run the following command to expose the `star_rating_snippet` product metafield:
138
-
139
- ```bash
140
- curl -X POST \
141
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
142
- -H 'Content-Type: application/graphql' \
143
- -H 'X-Shopify-Access-Token: {access_token}' \
144
- -d '
145
- mutation {
146
- metafieldDefinitionCreate(
147
- definition: {
148
- name: "StarRatingSnippet"
149
- namespace: "$app:reviews"
150
- key: "star_rating_snippet"
151
- type: "multi_line_text_field"
152
- ownerType: PRODUCT
153
- access: {
154
- admin: PUBLIC_READ
155
- storefront: PUBLIC_READ
156
- }
157
- }
158
- ) {
159
- createdDefinition { id name }
160
- userErrors { field message code }
161
- }
162
- }
163
- '
164
- ```
165
-
166
- 5. Run the following command to expose the `review_count` product metafield:
167
-
168
- ```bash
169
- curl -X POST \
170
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
171
- -H 'Content-Type: application/graphql' \
172
- -H 'X-Shopify-Access-Token: {access_token}' \
173
- -d '
174
- mutation {
175
- metafieldDefinitionCreate(
176
- definition: {
177
- name: "ReviewCount"
178
- namespace: "$app:reviews"
179
- key: "review_count"
180
- type: "number_integer"
181
- ownerType: PRODUCT
182
- access: {
183
- admin: PUBLIC_READ
184
- storefront: PUBLIC_READ
185
- }
186
- }
187
- ) {
188
- createdDefinition { id name }
189
- userErrors { field message code }
190
- }
191
- }
192
- '
193
- ```
194
-
195
- 6. Run the following command to expose the `average_rating` product metafield:
196
-
197
- ```bash
198
- curl -X POST \
199
- https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
200
- -H 'Content-Type: application/graphql' \
201
- -H 'X-Shopify-Access-Token: {access_token}' \
202
- -d '
203
- mutation {
204
- metafieldDefinitionCreate(
205
- definition: {
206
- name: "AverageRating"
207
- namespace: "$app:reviews"
208
- key: "average_rating"
209
- type: "rating"
210
- ownerType: PRODUCT
211
- access: {
212
- admin: PUBLIC_READ
213
- storefront: PUBLIC_READ
214
- }
215
- }
216
- ) {
217
- createdDefinition { id name }
218
- userErrors { field message code }
219
- }
220
- }
221
- '
222
- ```
223
-
224
- ### Using GraphQL IDE
225
-
226
- Open your GraphQL IDE (such as Postman) and make `POST` requests with the following details:
227
-
228
- - **URL:** https://{shop}.myshopify.com/admin/api/2024-10/graphql.json
229
- - **Headers:** - X-Shopify-Access-Token: {access_token} - Content-Type: application/json
230
-
231
- 1. Execute the following request to expose the `widget_pre_render_style_tags` shop metafield:
232
-
233
- ```graphql
234
- mutation {
235
- metafieldDefinitionCreate(
236
- definition: {
237
- name: "WidgetPreRenderStyleTags"
238
- namespace: "$app:reviews"
239
- key: "widget_pre_render_style_tags"
240
- type: "multi_line_text_field"
241
- ownerType: SHOP
242
- access: {
243
- admin: PUBLIC_READ
244
- storefront: PUBLIC_READ
245
- }
246
- }
247
- ) {
248
- createdDefinition {
249
- id
250
- name
251
- }
252
- userErrors {
253
- field
254
- message
255
- code
256
- }
257
- }
258
- }
259
- ```
260
-
261
- 2. Execute the following request to expose the `widget_pre_render_body_style_tags` shop metafield:
262
-
263
- ```graphql
264
- mutation {
265
- metafieldDefinitionCreate(
266
- definition: {
267
- name: "WidgetPreRenderBodyStyleTags"
268
- namespace: "$app:reviews"
269
- key: "widget_pre_render_body_style_tags"
270
- type: "multi_line_text_field"
271
- ownerType: SHOP
272
- access: {
273
- admin: PUBLIC_READ
274
- storefront: PUBLIC_READ
275
- }
276
- }
277
- ) {
278
- createdDefinition {
279
- id
280
- name
281
- }
282
- userErrors {
283
- field
284
- message
285
- code
286
- }
287
- }
288
- }
289
- ```
290
-
291
- 3. Execute the following request to expose the `reviews_widget_snippet` product metafield:
292
-
293
- ```graphql
294
- mutation {
295
- metafieldDefinitionCreate(
296
- definition: {
297
- name: "ReviewsWidgetSnippet"
298
- namespace: "$app:reviews"
299
- key: "reviews_widget_snippet"
300
- type: "multi_line_text_field"
301
- ownerType: PRODUCT
302
- access: {
303
- admin: PUBLIC_READ
304
- storefront: PUBLIC_READ
305
- }
306
- }
307
- ) {
308
- createdDefinition {
309
- id
310
- name
311
- }
312
- userErrors {
313
- field
314
- message
315
- code
316
- }
317
- }
318
- }
319
- ```
320
-
321
- 4. Execute the following request to expose the `star_rating_snippet` product metafield:
322
-
323
- ```graphql
324
- mutation {
325
- metafieldDefinitionCreate(
326
- definition: {
327
- name: "StarRatingSnippet"
328
- namespace: "$app:reviews"
329
- key: "star_rating_snippet"
330
- type: "multi_line_text_field"
331
- ownerType: PRODUCT
332
- access: {
333
- admin: PUBLIC_READ
334
- storefront: PUBLIC_READ
335
- }
336
- }
337
- ) {
338
- createdDefinition {
339
- id
340
- name
341
- }
342
- userErrors {
343
- field
344
- message
345
- code
346
- }
347
- }
348
- }
349
- ```
350
-
351
- 5. Execute the following request to expose the `review_count` product metafield:
352
-
353
- ```graphql
354
- mutation {
355
- metafieldDefinitionCreate(
356
- definition: {
357
- name: "ReviewCount"
358
- namespace: "$app:reviews"
359
- key: "review_count"
360
- type: "number_integer"
361
- ownerType: PRODUCT
362
- access: {
363
- admin: PUBLIC_READ
364
- storefront: PUBLIC_READ
365
- }
366
- }
367
- ) {
368
- createdDefinition {
369
- id
370
- name
371
- }
372
- userErrors {
373
- field
374
- message
375
- code
376
- }
377
- }
378
- }
379
- ```
380
-
381
- 6. Execute the following request to expose the `average_rating` product metafield:
382
-
383
- ```graphql
384
- mutation {
385
- metafieldDefinitionCreate(
386
- definition: {
387
- name: "AverageRating"
388
- namespace: "$app:reviews"
389
- key: "average_rating"
390
- type: "rating"
391
- ownerType: PRODUCT
392
- access: {
393
- admin: PUBLIC_READ
394
- storefront: PUBLIC_READ
395
- }
396
- }
397
- ) {
398
- createdDefinition {
399
- id
400
- name
401
- }
402
- userErrors {
403
- field
404
- message
405
- code
406
- }
407
- }
408
- }
409
- ```
410
-
411
- **References**
412
-
413
- - [https://shopify.dev/api/examples/metafields#step-1-expose-metafields](https://shopify.dev/api/examples/metafields#step-1-expose-metafields)
414
- - [https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate](https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate)
415
-
416
- </details>
417
-
418
- ## Installation
419
-
420
- This package provides:
421
-
422
- - one function: `getOkendoProviderData`,
423
- - one provider: `OkendoProvider`,
424
- - two React components: `OkendoStarRating` and `OkendoReviews`.
425
-
426
- 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.
427
-
428
- > 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.
429
-
430
- 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.
431
-
432
- > 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).
433
-
434
- Run:
435
-
436
- ```bash
437
- npm i @okendo/shopify-hydrogen
438
- ```
439
-
440
- ### `app/root.tsx`
441
-
442
- `OkendoProvider` supports two ways of loading the data returned by `getOkendoProviderData`:
443
-
444
- - **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.
445
- - **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.
446
-
447
- To summarise the differences between the two behaviours:
448
-
449
- - Pass the promise to `OkendoProvider` — so don't use `await` with `getOkendoProviderData`:
450
-
451
- - The page loading time won't be increased at all.
452
- - The widgets will be rendered client-side.
453
- - Placeholders (which are customisable) are shown until the widgets are rendered.
454
-
455
- - Pass the data to `OkendoProvider` — so use `await` with `getOkendoProviderData`:
456
- - The page loading time can be increased by a couple hundreds milliseconds.
457
- - The widgets will be rendered server-side.
458
- - The widgets are shown as soon as the page loads — no placeholders needed.
459
-
460
- You can easily experiment with the two ways, and decide which is the one you'd like to keep for your store.
461
-
462
- Open `app/root.tsx` and add the following import:
463
-
464
- ```ts
465
- import {
466
- OkendoProvider,
467
- getOkendoProviderData,
468
- } from "@okendo/shopify-hydrogen";
469
- ```
470
-
471
- Locate the `loader` function, append `okendoProviderData` to the returned data as shown below, and set `subscriberId` to your Okendo subscriber ID.
472
-
473
- As explained above, set `okendoProviderData` to either `getOkendoProviderData(...)`, or `await getOkendoProviderData(...)`:
474
-
475
- ```ts
476
- return defer(
477
- {
478
- ...
479
- okendoProviderData: /* place `await` here if you want server-rendered widgets */ getOkendoProviderData({
480
- context,
481
- subscriberId: "<your-okendo-subscriber-id>",
482
- }),
483
- },
484
- );
485
- ```
486
-
487
- Locate the `App` function, add the `meta` tag `oke:subscriber_id` to `head`, and place your Okendo subscriber ID in its content:
488
-
489
- ```ts
490
- <head>
491
- <meta charSet="utf-8" />
492
- <meta name="viewport" content="width=device-width,initial-scale=1" />
493
- <meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
494
- ...
495
- ```
496
-
497
- 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):
498
-
499
- ```tsx
500
- ...
501
- <body>
502
- <OkendoProvider
503
- nonce={nonce}
504
- okendoProviderData={data.okendoProviderData}
505
- >
506
- ...
507
- </OkendoProvider>
508
- </body>
509
- ...
510
- ```
511
-
512
- ### `app/entry.server.tsx`
513
-
514
- > This is only necessary if Content Security Policy is active in your project.
515
-
516
- Locate the call to `createContentSecurityPolicy`, and ensure your configuration includes the entries below:
517
- 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:
518
-
519
- ```ts
520
- const { nonce, header, NonceProvider } = createContentSecurityPolicy({
521
- defaultSrc: [
522
- "'self'",
523
- "localhost:*",
524
- "https://cdn.shopify.com",
525
- "https://www.google.com",
526
- "https://www.gstatic.com",
527
- "https://d3hw6dc1ow8pp2.cloudfront.net",
528
- "https://d3g5hqndtiniji.cloudfront.net",
529
- "https://dov7r31oq5dkj.cloudfront.net",
530
- "https://cdn-static.okendo.io",
531
- "https://surveys.okendo.io",
532
- "https://api.okendo.io",
533
- "data:",
534
- ],
535
- imgSrc: [
536
- "'self'",
537
- "https://cdn.shopify.com",
538
- "data:",
539
- "https://d3hw6dc1ow8pp2.cloudfront.net",
540
- "https://d3g5hqndtiniji.cloudfront.net",
541
- "https://dov7r31oq5dkj.cloudfront.net",
542
- "https://cdn-static.okendo.io",
543
- "https://surveys.okendo.io"
544
- ],
545
- mediaSrc: [
546
- "'self'",
547
- "https://d3hw6dc1ow8pp2.cloudfront.net",
548
- "https://d3g5hqndtiniji.cloudfront.net",
549
- "https://dov7r31oq5dkj.cloudfront.net",
550
- "https://cdn-static.okendo.io"
551
- ],
552
- styleSrcElem: [
553
- "'self'",
554
- "'unsafe-inline'",
555
- "https://cdn.shopify.com",
556
- "https://fonts.googleapis.com",
557
- "https://fonts.gstatic.com",
558
- "https://d3hw6dc1ow8pp2.cloudfront.net",
559
- "https://cdn-static.okendo.io",
560
- "https://surveys.okendo.io"
561
- ],
562
- scriptSrc: [
563
- "'self'",
564
- "https://cdn.shopify.com",
565
- "https://d3hw6dc1ow8pp2.cloudfront.net",
566
- "https://dov7r31oq5dkj.cloudfront.net",
567
- "https://cdn-static.okendo.io",
568
- "https://surveys.okendo.io",
569
- "https://api.okendo.io",
570
- "https://www.google.com",
571
- "https://www.gstatic.com"
572
- ],
573
- fontSrc: [
574
- "'self'",
575
- "https://fonts.gstatic.com",
576
- "https://d3hw6dc1ow8pp2.cloudfront.net",
577
- "https://dov7r31oq5dkj.cloudfront.net",
578
- "https://cdn.shopify.com",
579
- "https://cdn-static.okendo.io",
580
- "https://surveys.okendo.io"
581
- ],
582
- connectSrc: [
583
- "'self'",
584
- "https://monorail-edge.shopifysvc.com",
585
- "localhost:*",
586
- "ws://localhost:*",
587
- "ws://127.0.0.1:*",
588
- "https://api.okendo.io",
589
- "https://cdn-static.okendo.io",
590
- "https://surveys.okendo.io",
591
- "https://api.raygun.com",
592
- "https://www.google.com",
593
- "https://www.gstatic.com",
594
- ],
595
- frameSrc: [
596
- "https://www.google.com",
597
- "https://www.gstatic.com"
598
- ]
599
- });
600
- ```
601
-
602
- ### `app/routes/_index.tsx`
603
-
604
- Add the following imports:
605
-
606
- ```ts
607
- import {
608
- OkendoStarRating,
609
- type WithOkendoStarRatingSnippet,
610
- } from "@okendo/shopify-hydrogen";
611
- ```
612
-
613
- Add the following block just before the `RECOMMENDED_PRODUCTS_QUERY` GraphQL query:
614
-
615
- ```ts
616
- const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
617
- fragment OkendoStarRatingSnippet on Product {
618
- okendoStarRatingSnippet: metafield(
619
- namespace: "okendo"
620
- key: "StarRatingSnippet"
621
- ) {
622
- value
623
- }
624
- }
625
- ` as const;
626
- ```
627
-
628
- Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `RECOMMENDED_PRODUCTS_QUERY`:
629
-
630
- ```ts
631
- const RECOMMENDED_PRODUCTS_QUERY = `#graphql
632
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
633
- fragment RecommendedProduct on Product {
634
- id
635
- title
636
- handle
637
- priceRange {
638
- minVariantPrice {
639
- amount
640
- currencyCode
641
- }
642
- }
643
- images(first: 1) {
644
- nodes {
645
- id
646
- url
647
- altText
648
- width
649
- height
650
- }
651
- }
652
- ...OkendoStarRatingSnippet
653
- }
654
- query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
655
- @inContext(country: $country, language: $language) {
656
- products(first: 4, sortKey: UPDATED_AT, reverse: true) {
657
- nodes {
658
- ...RecommendedProduct
659
- }
660
- }
661
- }
662
- ` as const;
663
- ```
664
-
665
- Tweak the type of the `products` prop of `RecommendedProducts`:
666
-
667
- ```ts
668
- products: Promise<{
669
- products: {
670
- nodes: (RecommendedProductsQuery["products"]["nodes"][0] &
671
- WithOkendoStarRatingSnippet)[];
672
- };
673
- }>;
674
- ```
675
-
676
- Add `OkendoStarRating` to `RecommendedProducts`:
677
-
678
- ```tsx
679
- <OkendoStarRating
680
- productId={product.id}
681
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
682
- />
683
- ```
684
-
685
- For instance, we can add it below the product title, like this:
686
-
687
- ```tsx
688
- <Image
689
- data={product.images.nodes[0]}
690
- aspectRatio="1/1"
691
- sizes="(min-width: 45em) 20vw, 50vw"
692
- />
693
- <h4>{product.title}</h4>
694
- <OkendoStarRating
695
- productId={product.id}
696
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
697
- />
698
- <small>
699
- <Money data={product.priceRange.minVariantPrice} />
700
- </small>
701
- ```
702
-
703
- > 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`.
704
-
705
- We now have the Okendo Star Rating widget visible on our page:
706
-
707
- ![Okendo's Star Rating widget](./okendo-star-rating-widget.png)
708
-
709
- ### `app/routes/products.$handle.tsx`
710
-
711
- Add the following imports:
712
-
713
- ```ts
714
- import {
715
- OKENDO_PRODUCT_REVIEWS_FRAGMENT,
716
- OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
717
- OkendoReviews,
718
- OkendoStarRating,
719
- type WithOkendoReviewsSnippet,
720
- type WithOkendoStarRatingSnippet,
721
- } from "@okendo/shopify-hydrogen";
722
- ```
723
-
724
- Add the following block just before the `RECOMMENDED_PRODUCTS_QUERY` GraphQL query:
725
-
726
- ```ts
727
- const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
728
- fragment OkendoStarRatingSnippet on Product {
729
- okendoStarRatingSnippet: metafield(
730
- namespace: "okendo"
731
- key: "StarRatingSnippet"
732
- ) {
733
- value
734
- }
735
- }
736
- ` as const;
737
-
738
- const OKENDO_PRODUCT_REVIEWS_FRAGMENT = `#graphql
739
- fragment OkendoReviewsSnippet on Product {
740
- okendoReviewsSnippet: metafield(
741
- namespace: "okendo"
742
- key: "ReviewsWidgetSnippet"
743
- ) {
744
- value
745
- }
746
- }
747
- ` as const;
748
- ```
749
-
750
- Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}`, `${OKENDO_PRODUCT_REVIEWS_FRAGMENT}`, `...OkendoStarRatingSnippet`, and `...OkendoReviewsSnippet` to `PRODUCT_FRAGMENT`:
751
-
752
- ```ts
753
- const PRODUCT_FRAGMENT = `#graphql
754
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
755
- ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
756
- fragment Product on Product {
757
- id
758
- title
759
- vendor
760
- handle
761
- descriptionHtml
762
- description
763
- options {
764
- name
765
- values
766
- }
767
- selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
768
- ...ProductVariant
769
- }
770
- variants(first: 1) {
771
- nodes {
772
- ...ProductVariant
773
- }
774
- }
775
- seo {
776
- description
777
- title
778
- }
779
- ...OkendoStarRatingSnippet
780
- ...OkendoReviewsSnippet
781
- }
782
- ${PRODUCT_VARIANT_FRAGMENT}
783
- ` as const;
784
- ```
785
-
786
- Add `OkendoReviews` to `Product`:
787
-
788
- ```tsx
789
- <OkendoReviews
790
- productId={product.id}
791
- okendoReviewsSnippet={product.okendoReviewsSnippet}
792
- />
793
- ```
794
-
795
- For instance, we can add it below the product section, like this:
796
-
797
- ```tsx
798
- <>
799
- <div className="product">
800
- <ProductImage image={selectedVariant?.image} />
801
- <ProductMain
802
- selectedVariant={selectedVariant}
803
- product={product}
804
- variants={variants}
805
- />
806
- </div>
807
-
808
- <OkendoReviews
809
- productId={product.id}
810
- okendoReviewsSnippet={product.okendoReviewsSnippet}
811
- />
812
- </>
813
- ```
814
-
815
- > 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 `OkendoReviews`.
816
-
817
- Tweak the type of the `product` prop of `ProductMain`:
818
-
819
- ```ts
820
- product: ProductFragment &
821
- WithOkendoStarRatingSnippet &
822
- WithOkendoReviewsSnippet;
823
- ```
824
-
825
- Add `OkendoStarRating` to `ProductMain`:
826
-
827
- ```tsx
828
- <OkendoStarRating
829
- productId={product.id}
830
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
831
- />
832
- ```
833
-
834
- For instance, we can add it below the product title, like this:
835
-
836
- ```tsx
837
- <div className="product-main">
838
- <h1>{title}</h1>
839
- <OkendoStarRating
840
- productId={product.id}
841
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
842
- />
843
- <ProductPrice selectedVariant={selectedVariant} />
844
- ```
845
-
846
- We now have the Okendo Star Rating and Reviews widgets visible on our product page:
847
-
848
- ![Okendo's Star Rating and Reviews widgets](./okendo-star-rating-and-reviews-widgets.png)
849
-
850
- ### All Reviews Widget - Client Side Only
851
- 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`.
852
-
853
- Please note the all reviews widget loads on the client not the server.
854
-
855
- ```tsx
856
- import { type MetaFunction } from '@remix-run/react';
857
- import { OkendoReviews } from '@okendo/shopify-hydrogen';
858
-
859
- export const meta: MetaFunction = () => {
860
- return [{title: `Hydrogen | Okendo All Reviews`}];
861
- };
862
-
863
- export default function ReviewsPage() {
864
- return (
865
- <div className="all-reviews">
866
- <h1>All Reviews Widget</h1>
867
- <OkendoReviews />
868
- </div>
869
- );
870
- }
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.3.0) of this package.
2
+
3
+ # Okendo Hydrogen 2 (Remix) React Components
4
+
5
+ This package brings [Okendo's review widgets](https://www.okendo.io/blog/widget-plus/) to a Shopify Hydrogen store.
6
+
7
+ ## Requirements
8
+
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.
13
+
14
+ ## Demo Store
15
+
16
+ Our demo store, which is based on the demo store provided by Shopify, can be found [here](https://github.com/okendo/okendo-shopify-hydrogen-demo).
17
+
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.
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
+
46
+ #### Using Curl
47
+
48
+ Open a new terminal or PowerShell window, then:
49
+
50
+ 1. Run the following command to expose the `widget_pre_render_style_tags` shop metafield:
51
+
52
+ ```bash
53
+ curl -X POST \
54
+ https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
55
+ -H 'Content-Type: application/graphql' \
56
+ -H 'X-Shopify-Access-Token: {access_token}' \
57
+ -d '
58
+ mutation {
59
+ metafieldDefinitionCreate(
60
+ definition: {
61
+ name: "WidgetPreRenderStyleTags"
62
+ namespace: "$app:review"
63
+ key: "widget_pre_render_style_tags"
64
+ type: "multi_line_text_field"
65
+ ownerType: SHOP
66
+ access: {
67
+ admin: PUBLIC_READ
68
+ storefront: PUBLIC_READ
69
+ }
70
+ }
71
+ ) {
72
+ createdDefinition { id name }
73
+ userErrors { field message code }
74
+ }
75
+ }
76
+ '
77
+ ```
78
+
79
+ 2. Run the following command to expose the `widget_pre_render_body_style_tags` shop metafield:
80
+
81
+ ```bash
82
+ curl -X POST \
83
+ https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
84
+ -H 'Content-Type: application/graphql' \
85
+ -H 'X-Shopify-Access-Token: {access_token}' \
86
+ -d '
87
+ mutation {
88
+ metafieldDefinitionCreate(
89
+ definition: {
90
+ name: "WidgetPreRenderBodyStyleTags"
91
+ namespace: "$app:review"
92
+ key: "widget_pre_render_body_style_tags"
93
+ type: "multi_line_text_field"
94
+ ownerType: SHOP
95
+ access: {
96
+ admin: PUBLIC_READ
97
+ storefront: PUBLIC_READ
98
+ }
99
+ }
100
+ ) {
101
+ createdDefinition { id name }
102
+ userErrors { field message code }
103
+ }
104
+ }
105
+ '
106
+ ```
107
+
108
+ 3. Run the following command to expose the `reviews_widget_snippet` product metafield:
109
+
110
+ ```bash
111
+ curl -X POST \
112
+ https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
113
+ -H 'Content-Type: application/graphql' \
114
+ -H 'X-Shopify-Access-Token: {access_token}' \
115
+ -d '
116
+ mutation {
117
+ metafieldDefinitionCreate(
118
+ definition: {
119
+ name: "ReviewsWidgetSnippet"
120
+ namespace: "$app:reviews"
121
+ key: "reviews_widget_snippet"
122
+ type: "multi_line_text_field"
123
+ ownerType: PRODUCT
124
+ access: {
125
+ admin: PUBLIC_READ
126
+ storefront: PUBLIC_READ
127
+ }
128
+ }
129
+ ) {
130
+ createdDefinition { id name }
131
+ userErrors { field message code }
132
+ }
133
+ }
134
+ '
135
+ ```
136
+
137
+ 4. Run the following command to expose the `star_rating_snippet` product metafield:
138
+
139
+ ```bash
140
+ curl -X POST \
141
+ https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
142
+ -H 'Content-Type: application/graphql' \
143
+ -H 'X-Shopify-Access-Token: {access_token}' \
144
+ -d '
145
+ mutation {
146
+ metafieldDefinitionCreate(
147
+ definition: {
148
+ name: "StarRatingSnippet"
149
+ namespace: "$app:reviews"
150
+ key: "star_rating_snippet"
151
+ type: "multi_line_text_field"
152
+ ownerType: PRODUCT
153
+ access: {
154
+ admin: PUBLIC_READ
155
+ storefront: PUBLIC_READ
156
+ }
157
+ }
158
+ ) {
159
+ createdDefinition { id name }
160
+ userErrors { field message code }
161
+ }
162
+ }
163
+ '
164
+ ```
165
+
166
+ 5. Run the following command to expose the `review_count` product metafield:
167
+
168
+ ```bash
169
+ curl -X POST \
170
+ https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
171
+ -H 'Content-Type: application/graphql' \
172
+ -H 'X-Shopify-Access-Token: {access_token}' \
173
+ -d '
174
+ mutation {
175
+ metafieldDefinitionCreate(
176
+ definition: {
177
+ name: "ReviewCount"
178
+ namespace: "$app:reviews"
179
+ key: "review_count"
180
+ type: "number_integer"
181
+ ownerType: PRODUCT
182
+ access: {
183
+ admin: PUBLIC_READ
184
+ storefront: PUBLIC_READ
185
+ }
186
+ }
187
+ ) {
188
+ createdDefinition { id name }
189
+ userErrors { field message code }
190
+ }
191
+ }
192
+ '
193
+ ```
194
+
195
+ 6. Run the following command to expose the `average_rating` product metafield:
196
+
197
+ ```bash
198
+ curl -X POST \
199
+ https://{shop}.myshopify.com/admin/api/2024-10/graphql.json \
200
+ -H 'Content-Type: application/graphql' \
201
+ -H 'X-Shopify-Access-Token: {access_token}' \
202
+ -d '
203
+ mutation {
204
+ metafieldDefinitionCreate(
205
+ definition: {
206
+ name: "AverageRating"
207
+ namespace: "$app:reviews"
208
+ key: "average_rating"
209
+ type: "rating"
210
+ ownerType: PRODUCT
211
+ access: {
212
+ admin: PUBLIC_READ
213
+ storefront: PUBLIC_READ
214
+ }
215
+ }
216
+ ) {
217
+ createdDefinition { id name }
218
+ userErrors { field message code }
219
+ }
220
+ }
221
+ '
222
+ ```
223
+
224
+ ### Using GraphQL IDE
225
+
226
+ Open your GraphQL IDE (such as Postman) and make `POST` requests with the following details:
227
+
228
+ - **URL:** https://{shop}.myshopify.com/admin/api/2024-10/graphql.json
229
+ - **Headers:** - X-Shopify-Access-Token: {access_token} - Content-Type: application/json
230
+
231
+ 1. Execute the following request to expose the `widget_pre_render_style_tags` shop metafield:
232
+
233
+ ```graphql
234
+ mutation {
235
+ metafieldDefinitionCreate(
236
+ definition: {
237
+ name: "WidgetPreRenderStyleTags"
238
+ namespace: "$app:reviews"
239
+ key: "widget_pre_render_style_tags"
240
+ type: "multi_line_text_field"
241
+ ownerType: SHOP
242
+ access: {
243
+ admin: PUBLIC_READ
244
+ storefront: PUBLIC_READ
245
+ }
246
+ }
247
+ ) {
248
+ createdDefinition {
249
+ id
250
+ name
251
+ }
252
+ userErrors {
253
+ field
254
+ message
255
+ code
256
+ }
257
+ }
258
+ }
259
+ ```
260
+
261
+ 2. Execute the following request to expose the `widget_pre_render_body_style_tags` shop metafield:
262
+
263
+ ```graphql
264
+ mutation {
265
+ metafieldDefinitionCreate(
266
+ definition: {
267
+ name: "WidgetPreRenderBodyStyleTags"
268
+ namespace: "$app:reviews"
269
+ key: "widget_pre_render_body_style_tags"
270
+ type: "multi_line_text_field"
271
+ ownerType: SHOP
272
+ access: {
273
+ admin: PUBLIC_READ
274
+ storefront: PUBLIC_READ
275
+ }
276
+ }
277
+ ) {
278
+ createdDefinition {
279
+ id
280
+ name
281
+ }
282
+ userErrors {
283
+ field
284
+ message
285
+ code
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+ 3. Execute the following request to expose the `reviews_widget_snippet` product metafield:
292
+
293
+ ```graphql
294
+ mutation {
295
+ metafieldDefinitionCreate(
296
+ definition: {
297
+ name: "ReviewsWidgetSnippet"
298
+ namespace: "$app:reviews"
299
+ key: "reviews_widget_snippet"
300
+ type: "multi_line_text_field"
301
+ ownerType: PRODUCT
302
+ access: {
303
+ admin: PUBLIC_READ
304
+ storefront: PUBLIC_READ
305
+ }
306
+ }
307
+ ) {
308
+ createdDefinition {
309
+ id
310
+ name
311
+ }
312
+ userErrors {
313
+ field
314
+ message
315
+ code
316
+ }
317
+ }
318
+ }
319
+ ```
320
+
321
+ 4. Execute the following request to expose the `star_rating_snippet` product metafield:
322
+
323
+ ```graphql
324
+ mutation {
325
+ metafieldDefinitionCreate(
326
+ definition: {
327
+ name: "StarRatingSnippet"
328
+ namespace: "$app:reviews"
329
+ key: "star_rating_snippet"
330
+ type: "multi_line_text_field"
331
+ ownerType: PRODUCT
332
+ access: {
333
+ admin: PUBLIC_READ
334
+ storefront: PUBLIC_READ
335
+ }
336
+ }
337
+ ) {
338
+ createdDefinition {
339
+ id
340
+ name
341
+ }
342
+ userErrors {
343
+ field
344
+ message
345
+ code
346
+ }
347
+ }
348
+ }
349
+ ```
350
+
351
+ 5. Execute the following request to expose the `review_count` product metafield:
352
+
353
+ ```graphql
354
+ mutation {
355
+ metafieldDefinitionCreate(
356
+ definition: {
357
+ name: "ReviewCount"
358
+ namespace: "$app:reviews"
359
+ key: "review_count"
360
+ type: "number_integer"
361
+ ownerType: PRODUCT
362
+ access: {
363
+ admin: PUBLIC_READ
364
+ storefront: PUBLIC_READ
365
+ }
366
+ }
367
+ ) {
368
+ createdDefinition {
369
+ id
370
+ name
371
+ }
372
+ userErrors {
373
+ field
374
+ message
375
+ code
376
+ }
377
+ }
378
+ }
379
+ ```
380
+
381
+ 6. Execute the following request to expose the `average_rating` product metafield:
382
+
383
+ ```graphql
384
+ mutation {
385
+ metafieldDefinitionCreate(
386
+ definition: {
387
+ name: "AverageRating"
388
+ namespace: "$app:reviews"
389
+ key: "average_rating"
390
+ type: "rating"
391
+ ownerType: PRODUCT
392
+ access: {
393
+ admin: PUBLIC_READ
394
+ storefront: PUBLIC_READ
395
+ }
396
+ }
397
+ ) {
398
+ createdDefinition {
399
+ id
400
+ name
401
+ }
402
+ userErrors {
403
+ field
404
+ message
405
+ code
406
+ }
407
+ }
408
+ }
409
+ ```
410
+
411
+ **References**
412
+
413
+ - [https://shopify.dev/api/examples/metafields#step-1-expose-metafields](https://shopify.dev/api/examples/metafields#step-1-expose-metafields)
414
+ - [https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate](https://shopify.dev/docs/api/admin-graphql/2024-10/mutations/metafieldDefinitionCreate)
415
+
416
+ </details>
417
+
418
+ ## Installation
419
+
420
+ This package provides:
421
+
422
+ - one function: `getOkendoProviderData`,
423
+ - one provider: `OkendoProvider`,
424
+ - two React components: `OkendoStarRating` and `OkendoReviews`.
425
+
426
+ 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.
427
+
428
+ > 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.
429
+
430
+ 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.
431
+
432
+ > 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).
433
+
434
+ Run:
435
+
436
+ ```bash
437
+ npm i @okendo/shopify-hydrogen
438
+ ```
439
+
440
+ ### `app/root.tsx`
441
+
442
+ `OkendoProvider` supports two ways of loading the data returned by `getOkendoProviderData`:
443
+
444
+ - **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.
445
+ - **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.
446
+
447
+ To summarise the differences between the two behaviours:
448
+
449
+ - Pass the promise to `OkendoProvider` — so don't use `await` with `getOkendoProviderData`:
450
+
451
+ - The page loading time won't be increased at all.
452
+ - The widgets will be rendered client-side.
453
+ - Placeholders (which are customisable) are shown until the widgets are rendered.
454
+
455
+ - Pass the data to `OkendoProvider` — so use `await` with `getOkendoProviderData`:
456
+ - The page loading time can be increased by a couple hundreds milliseconds.
457
+ - The widgets will be rendered server-side.
458
+ - The widgets are shown as soon as the page loads — no placeholders needed.
459
+
460
+ You can easily experiment with the two ways, and decide which is the one you'd like to keep for your store.
461
+
462
+ Open `app/root.tsx` and add the following import:
463
+
464
+ ```ts
465
+ import {
466
+ OkendoProvider,
467
+ getOkendoProviderData,
468
+ } from "@okendo/shopify-hydrogen";
469
+ ```
470
+
471
+ Locate the `loader` function, append `okendoProviderData` to the returned data as shown below, and set `subscriberId` to your Okendo subscriber ID.
472
+
473
+ As explained above, set `okendoProviderData` to either `getOkendoProviderData(...)`, or `await getOkendoProviderData(...)`:
474
+
475
+ ```ts
476
+ return defer(
477
+ {
478
+ ...
479
+ okendoProviderData: /* place `await` here if you want server-rendered widgets */ getOkendoProviderData({
480
+ context,
481
+ subscriberId: "<your-okendo-subscriber-id>",
482
+ }),
483
+ },
484
+ );
485
+ ```
486
+
487
+ Locate the `App` function, add the `meta` tag `oke:subscriber_id` to `head`, and place your Okendo subscriber ID in its content:
488
+
489
+ ```ts
490
+ <head>
491
+ <meta charSet="utf-8" />
492
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
493
+ <meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
494
+ ...
495
+ ```
496
+
497
+ 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):
498
+
499
+ ```tsx
500
+ ...
501
+ <body>
502
+ <OkendoProvider
503
+ nonce={nonce}
504
+ okendoProviderData={data.okendoProviderData}
505
+ >
506
+ ...
507
+ </OkendoProvider>
508
+ </body>
509
+ ...
510
+ ```
511
+
512
+ ### `app/entry.server.tsx`
513
+
514
+ > This is only necessary if Content Security Policy is active in your project.
515
+
516
+ Locate the call to `createContentSecurityPolicy`, and ensure your configuration includes the entries below:
517
+ 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:
518
+
519
+ ```ts
520
+ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
521
+ defaultSrc: [
522
+ "'self'",
523
+ "localhost:*",
524
+ "https://cdn.shopify.com",
525
+ "https://www.google.com",
526
+ "https://www.gstatic.com",
527
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
528
+ "https://d3g5hqndtiniji.cloudfront.net",
529
+ "https://dov7r31oq5dkj.cloudfront.net",
530
+ "https://cdn-static.okendo.io",
531
+ "https://surveys.okendo.io",
532
+ "https://api.okendo.io",
533
+ "data:",
534
+ ],
535
+ imgSrc: [
536
+ "'self'",
537
+ "https://cdn.shopify.com",
538
+ "data:",
539
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
540
+ "https://d3g5hqndtiniji.cloudfront.net",
541
+ "https://dov7r31oq5dkj.cloudfront.net",
542
+ "https://cdn-static.okendo.io",
543
+ "https://surveys.okendo.io"
544
+ ],
545
+ mediaSrc: [
546
+ "'self'",
547
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
548
+ "https://d3g5hqndtiniji.cloudfront.net",
549
+ "https://dov7r31oq5dkj.cloudfront.net",
550
+ "https://cdn-static.okendo.io"
551
+ ],
552
+ styleSrcElem: [
553
+ "'self'",
554
+ "'unsafe-inline'",
555
+ "https://cdn.shopify.com",
556
+ "https://fonts.googleapis.com",
557
+ "https://fonts.gstatic.com",
558
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
559
+ "https://cdn-static.okendo.io",
560
+ "https://surveys.okendo.io"
561
+ ],
562
+ scriptSrc: [
563
+ "'self'",
564
+ "https://cdn.shopify.com",
565
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
566
+ "https://dov7r31oq5dkj.cloudfront.net",
567
+ "https://cdn-static.okendo.io",
568
+ "https://surveys.okendo.io",
569
+ "https://api.okendo.io",
570
+ "https://www.google.com",
571
+ "https://www.gstatic.com"
572
+ ],
573
+ fontSrc: [
574
+ "'self'",
575
+ "https://fonts.gstatic.com",
576
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
577
+ "https://dov7r31oq5dkj.cloudfront.net",
578
+ "https://cdn.shopify.com",
579
+ "https://cdn-static.okendo.io",
580
+ "https://surveys.okendo.io"
581
+ ],
582
+ connectSrc: [
583
+ "'self'",
584
+ "https://monorail-edge.shopifysvc.com",
585
+ "localhost:*",
586
+ "ws://localhost:*",
587
+ "ws://127.0.0.1:*",
588
+ "https://api.okendo.io",
589
+ "https://cdn-static.okendo.io",
590
+ "https://surveys.okendo.io",
591
+ "https://api.raygun.com",
592
+ "https://www.google.com",
593
+ "https://www.gstatic.com",
594
+ ],
595
+ frameSrc: [
596
+ "https://www.google.com",
597
+ "https://www.gstatic.com"
598
+ ]
599
+ });
600
+ ```
601
+
602
+ ### `app/routes/_index.tsx`
603
+
604
+ Add the following imports:
605
+
606
+ ```ts
607
+ import {
608
+ OkendoStarRating,
609
+ type WithOkendoStarRatingSnippet,
610
+ } from "@okendo/shopify-hydrogen";
611
+ ```
612
+
613
+ Add the following block just before the `RECOMMENDED_PRODUCTS_QUERY` GraphQL query:
614
+
615
+ ```ts
616
+ const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
617
+ fragment OkendoStarRatingSnippet on Product {
618
+ okendoStarRatingSnippet: metafield(
619
+ namespace: "okendo"
620
+ key: "StarRatingSnippet"
621
+ ) {
622
+ value
623
+ }
624
+ }
625
+ ` as const;
626
+ ```
627
+
628
+ Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `RECOMMENDED_PRODUCTS_QUERY`:
629
+
630
+ ```ts
631
+ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
632
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
633
+ fragment RecommendedProduct on Product {
634
+ id
635
+ title
636
+ handle
637
+ priceRange {
638
+ minVariantPrice {
639
+ amount
640
+ currencyCode
641
+ }
642
+ }
643
+ images(first: 1) {
644
+ nodes {
645
+ id
646
+ url
647
+ altText
648
+ width
649
+ height
650
+ }
651
+ }
652
+ ...OkendoStarRatingSnippet
653
+ }
654
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
655
+ @inContext(country: $country, language: $language) {
656
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
657
+ nodes {
658
+ ...RecommendedProduct
659
+ }
660
+ }
661
+ }
662
+ ` as const;
663
+ ```
664
+
665
+ Tweak the type of the `products` prop of `RecommendedProducts`:
666
+
667
+ ```ts
668
+ products: Promise<{
669
+ products: {
670
+ nodes: (RecommendedProductsQuery["products"]["nodes"][0] &
671
+ WithOkendoStarRatingSnippet)[];
672
+ };
673
+ }>;
674
+ ```
675
+
676
+ Add `OkendoStarRating` to `RecommendedProducts`:
677
+
678
+ ```tsx
679
+ <OkendoStarRating
680
+ productId={product.id}
681
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
682
+ />
683
+ ```
684
+
685
+ For instance, we can add it below the product title, like this:
686
+
687
+ ```tsx
688
+ <Image
689
+ data={product.images.nodes[0]}
690
+ aspectRatio="1/1"
691
+ sizes="(min-width: 45em) 20vw, 50vw"
692
+ />
693
+ <h4>{product.title}</h4>
694
+ <OkendoStarRating
695
+ productId={product.id}
696
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
697
+ />
698
+ <small>
699
+ <Money data={product.priceRange.minVariantPrice} />
700
+ </small>
701
+ ```
702
+
703
+ > 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`.
704
+
705
+ We now have the Okendo Star Rating widget visible on our page:
706
+
707
+ ![Okendo's Star Rating widget](./okendo-star-rating-widget.png)
708
+
709
+ ### `app/routes/products.$handle.tsx`
710
+
711
+ Add the following imports:
712
+
713
+ ```ts
714
+ import {
715
+ OKENDO_PRODUCT_REVIEWS_FRAGMENT,
716
+ OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
717
+ OkendoReviews,
718
+ OkendoStarRating,
719
+ type WithOkendoReviewsSnippet,
720
+ type WithOkendoStarRatingSnippet,
721
+ } from "@okendo/shopify-hydrogen";
722
+ ```
723
+
724
+ Add the following block just before the `RECOMMENDED_PRODUCTS_QUERY` GraphQL query:
725
+
726
+ ```ts
727
+ const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = `#graphql
728
+ fragment OkendoStarRatingSnippet on Product {
729
+ okendoStarRatingSnippet: metafield(
730
+ namespace: "okendo"
731
+ key: "StarRatingSnippet"
732
+ ) {
733
+ value
734
+ }
735
+ }
736
+ ` as const;
737
+
738
+ const OKENDO_PRODUCT_REVIEWS_FRAGMENT = `#graphql
739
+ fragment OkendoReviewsSnippet on Product {
740
+ okendoReviewsSnippet: metafield(
741
+ namespace: "okendo"
742
+ key: "ReviewsWidgetSnippet"
743
+ ) {
744
+ value
745
+ }
746
+ }
747
+ ` as const;
748
+ ```
749
+
750
+ Then append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}`, `${OKENDO_PRODUCT_REVIEWS_FRAGMENT}`, `...OkendoStarRatingSnippet`, and `...OkendoReviewsSnippet` to `PRODUCT_FRAGMENT`:
751
+
752
+ ```ts
753
+ const PRODUCT_FRAGMENT = `#graphql
754
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
755
+ ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
756
+ fragment Product on Product {
757
+ id
758
+ title
759
+ vendor
760
+ handle
761
+ descriptionHtml
762
+ description
763
+ options {
764
+ name
765
+ values
766
+ }
767
+ selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
768
+ ...ProductVariant
769
+ }
770
+ variants(first: 1) {
771
+ nodes {
772
+ ...ProductVariant
773
+ }
774
+ }
775
+ seo {
776
+ description
777
+ title
778
+ }
779
+ ...OkendoStarRatingSnippet
780
+ ...OkendoReviewsSnippet
781
+ }
782
+ ${PRODUCT_VARIANT_FRAGMENT}
783
+ ` as const;
784
+ ```
785
+
786
+ Add `OkendoReviews` to `Product`:
787
+
788
+ ```tsx
789
+ <OkendoReviews
790
+ productId={product.id}
791
+ okendoReviewsSnippet={product.okendoReviewsSnippet}
792
+ />
793
+ ```
794
+
795
+ For instance, we can add it below the product section, like this:
796
+
797
+ ```tsx
798
+ <>
799
+ <div className="product">
800
+ <ProductImage image={selectedVariant?.image} />
801
+ <ProductMain
802
+ selectedVariant={selectedVariant}
803
+ product={product}
804
+ variants={variants}
805
+ />
806
+ </div>
807
+
808
+ <OkendoReviews
809
+ productId={product.id}
810
+ okendoReviewsSnippet={product.okendoReviewsSnippet}
811
+ />
812
+ </>
813
+ ```
814
+
815
+ > 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 `OkendoReviews`.
816
+
817
+ Tweak the type of the `product` prop of `ProductMain`:
818
+
819
+ ```ts
820
+ product: ProductFragment &
821
+ WithOkendoStarRatingSnippet &
822
+ WithOkendoReviewsSnippet;
823
+ ```
824
+
825
+ Add `OkendoStarRating` to `ProductMain`:
826
+
827
+ ```tsx
828
+ <OkendoStarRating
829
+ productId={product.id}
830
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
831
+ />
832
+ ```
833
+
834
+ For instance, we can add it below the product title, like this:
835
+
836
+ ```tsx
837
+ <div className="product-main">
838
+ <h1>{title}</h1>
839
+ <OkendoStarRating
840
+ productId={product.id}
841
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
842
+ />
843
+ <ProductPrice selectedVariant={selectedVariant} />
844
+ ```
845
+
846
+ We now have the Okendo Star Rating and Reviews widgets visible on our product page:
847
+
848
+ ![Okendo's Star Rating and Reviews widgets](./okendo-star-rating-and-reviews-widgets.png)
849
+
850
+ ### All Reviews Widget - Client Side Only
851
+ 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`.
852
+
853
+ Please note the all reviews widget loads on the client not the server.
854
+
855
+ ```tsx
856
+ import { type MetaFunction } from '@remix-run/react';
857
+ import { OkendoReviews } from '@okendo/shopify-hydrogen';
858
+
859
+ export const meta: MetaFunction = () => {
860
+ return [{title: `Hydrogen | Okendo All Reviews`}];
861
+ };
862
+
863
+ export default function ReviewsPage() {
864
+ return (
865
+ <div className="all-reviews">
866
+ <h1>All Reviews Widget</h1>
867
+ <OkendoReviews />
868
+ </div>
869
+ );
870
+ }
871
871
  ```