@okendo/shopify-hydrogen 2.3.2 → 2.5.0

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