@okendo/shopify-hydrogen 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ This package brings [Okendo's review widgets](https://www.okendo.io/blog/widget-
6
6
 
7
7
  ## Requirements
8
8
 
9
- - A Shopify store with the **Okendo: Product Reviews & UCG** app installed and configured.
9
+ - A Shopify store with the [**Okendo: Product Reviews & UCG**](https://apps.shopify.com/okendo-reviews) app installed and configured.
10
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
11
  - A current Okendo subscription.
12
12
  - A [Storefront access token](https://github.com/okendo/okendo-shopify-hydrogen-demo/wiki/Configure-Shopify-Storefront-API).
@@ -16,6 +16,8 @@ This package brings [Okendo's review widgets](https://www.okendo.io/blog/widget-
16
16
 
17
17
  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).
18
18
 
19
+ > Note: there have been multiple versions of Shopify's Hydrogen demo store. If your project is based on an old version of it, consult our version history to find out how to add Okendo to it.
20
+
19
21
  ## How it works
20
22
 
21
23
  This package provides:
@@ -25,7 +27,7 @@ This package provides:
25
27
 
26
28
  The function `getOkendoProviderData` needs to be called in the `loader` function of the `root.tsx` file in the Hydrogen 2 store. The data is then retrieved in `App` through `useLoaderData` and provided to `OkendoProvider` which is added in the `body` of the HTML returned by `App`.
27
29
 
28
- 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 components `OkendoStarRating` and `OkendoReviews` can be added on the store pages. There are a few more bits of configuration to do, please see below.
29
31
 
30
32
  ## Expose Shopify Metafields <a id="expose-shopify-metafields" name="expose-shopify-metafields"></a>
31
33
 
@@ -254,7 +256,7 @@ mutation {
254
256
 
255
257
  ## Installation
256
258
 
257
- > The code examples provided in this section are based on the [demo store provided by Shopify](https://github.com/Shopify/hydrogen/tree/2023-01/templates/demo-store). You will find the following steps already done in [our demo store](https://github.com/okendo/okendo-shopify-hydrogen-demo).
259
+ > The code examples provided in this section are based on the [demo store provided by Shopify](https://github.com/Shopify/hydrogen/tree/main/templates/demo-store). You will find the following steps already done in [our demo store](https://github.com/okendo/okendo-shopify-hydrogen-demo).
258
260
 
259
261
  Run:
260
262
 
@@ -273,44 +275,11 @@ import {
273
275
  } from "@okendo/shopify-hydrogen";
274
276
  ```
275
277
 
276
- Locate the `meta` function, and append the property `oke:subscriber_id` containing your Okendo subscriber ID:
277
-
278
- ```ts
279
- 'oke:subscriber_id': '<your-okendo-subscriber-id>',
280
- ```
281
-
282
- The `meta` function should now look like the following:
283
-
284
- ```ts
285
- export const meta: MetaFunction = () => ({
286
- charset: "utf-8",
287
- viewport: "width=device-width,initial-scale=1",
288
- "oke:subscriber_id": "<your-okendo-subscriber-id>",
289
- });
290
- ```
291
-
292
- Locate the `loader` function and append the `okendoProviderData` property to the returned data:
293
-
294
- ```ts
295
- okendoProviderData: await getOkendoProviderData({
296
- context,
297
- subscriberId: '<your-okendo-subscriber-id>',
298
- }),
299
- ```
300
-
301
- The `loader` function returned data should now look like the following:
278
+ Locate the `loader` function, append the `okendoProviderData` property to the returned data, and set `subscriberId` to your Okendo subscriber ID:
302
279
 
303
280
  ```ts
304
281
  return defer({
305
- isLoggedIn: Boolean(customerAccessToken),
306
- layout,
307
- selectedLocale: context.storefront.i18n,
308
- cart: cartId ? getCart(context, cartId) : undefined,
309
- analytics: {
310
- shopifySalesChannel: ShopifySalesChannel.hydrogen,
311
- shopId: layout.shop.id,
312
- },
313
- seo,
282
+ ...
314
283
  okendoProviderData: await getOkendoProviderData({
315
284
  context,
316
285
  subscriberId: "<your-okendo-subscriber-id>",
@@ -318,78 +287,131 @@ return defer({
318
287
  });
319
288
  ```
320
289
 
321
- Locate the `App` function, append `OkendoProvider` to `body`, and provide it with the data returned by `getOkendoProviderData`:
290
+ Locate the `App` function, add the `meta` tag `oke:subscriber_id` to `head`, and place your Okendo subscriber ID in its content:
291
+
292
+ ```ts
293
+ <head>
294
+ <meta charSet="utf-8" />
295
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
296
+ <meta name="oke:subscriber_id" content="<your-okendo-subscriber-id>" />
297
+ ...
298
+ ```
299
+
300
+ Append `OkendoProvider` to `body`, and pass it 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):
322
301
 
323
302
  ```tsx
324
303
  ...
325
304
  <body>
326
- <OkendoProvider okendoProviderData={data.okendoProviderData} />
305
+ <OkendoProvider
306
+ nonce={nonce}
307
+ okendoProviderData={data.okendoProviderData}
308
+ />
327
309
  ...
328
310
  </body>
329
311
  ...
330
312
  ```
331
313
 
332
- ### `app/data/fragments.ts`
314
+ ### `app/entry.server.tsx`
333
315
 
334
- Open `app/data/fragments.ts` and add the following import:
316
+ > This is only necessary if Content Security Policy is active in your project.
335
317
 
336
- ```ts
337
- import { OKENDO_PRODUCT_STAR_RATING_FRAGMENT } from "@okendo/shopify-hydrogen";
338
- ```
318
+ Locate the call to `createContentSecurityPolicy`, and add:
319
+
320
+ - `https://d3hw6dc1ow8pp2.cloudfront.net`, `https://d3g5hqndtiniji.cloudfront.net`, and `data:` to `defaultSrc`
321
+ - `https://d3hw6dc1ow8pp2.cloudfront.net` to `styleSrc`
322
+ - `https://api.okendo.io` to `connectSrc`
339
323
 
340
- Then add `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` and `...OkendoStarRatingSnippet` to `PRODUCT_CARD_FRAGMENT`:
324
+ 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:
341
325
 
342
326
  ```ts
343
- export const PRODUCT_CARD_FRAGMENT = `#graphql
344
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
345
- fragment ProductCard on Product {
346
- id
347
- title
348
- publishedAt
349
- handle
350
- ...OkendoStarRatingSnippet
351
- variants(first: 1) {
352
- ...
327
+ const { nonce, header, NonceProvider } = createContentSecurityPolicy({
328
+ defaultSrc: [
329
+ "'self'",
330
+ "localhost:*",
331
+ "https://cdn.shopify.com",
332
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
333
+ "https://d3g5hqndtiniji.cloudfront.net",
334
+ "data:",
335
+ ],
336
+ styleSrc: [
337
+ "'self'",
338
+ "'unsafe-inline'",
339
+ "https://cdn.shopify.com",
340
+ "localhost:*",
341
+ "https://d3hw6dc1ow8pp2.cloudfront.net",
342
+ ],
343
+ connectSrc: [
344
+ "'self'",
345
+ "https://monorail-edge.shopifysvc.com",
346
+ "localhost:*",
347
+ "ws://localhost:*",
348
+ "ws://127.0.0.1:*",
349
+ "https://api.okendo.io",
350
+ ],
351
+ });
353
352
  ```
354
353
 
355
- ### `app/components/ProductCard.tsx`
354
+ ### `app/routes/_index.tsx`
356
355
 
357
- Open `app/components/ProductCard.tsx` and add the following import:
356
+ Add the following imports:
358
357
 
359
358
  ```ts
360
359
  import {
360
+ OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
361
361
  OkendoStarRating,
362
362
  WithOkendoStarRatingSnippet,
363
363
  } from "@okendo/shopify-hydrogen";
364
364
  ```
365
365
 
366
- Tweak the type of the `product` prop to be:
366
+ Locate the `RECOMMENDED_PRODUCTS_QUERY` GraphQL query, and append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}` `...OkendoStarRatingSnippet` to it:
367
367
 
368
368
  ```ts
369
- product: SerializeFrom<Product & WithOkendoStarRatingSnippet>;
369
+ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
370
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
371
+ fragment RecommendedProduct on Product {
372
+ id
373
+ title
374
+ handle
375
+ priceRange {
376
+ minVariantPrice {
377
+ amount
378
+ currencyCode
379
+ }
380
+ }
381
+ images(first: 1) {
382
+ nodes {
383
+ id
384
+ url
385
+ altText
386
+ width
387
+ height
388
+ }
389
+ }
390
+ ...OkendoStarRatingSnippet
391
+ }
392
+ query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
393
+ @inContext(country: $country, language: $language) {
394
+ products(first: 4, sortKey: UPDATED_AT, reverse: true) {
395
+ nodes {
396
+ ...RecommendedProduct
397
+ }
398
+ }
399
+ }
400
+ ` as const;
370
401
  ```
371
402
 
372
- `ProductCard` should now look like this:
403
+ Tweak the type of the `products` prop of `RecommendedProducts`:
373
404
 
374
405
  ```ts
375
- export function ProductCard({
376
- product,
377
- label,
378
- className,
379
- loading,
380
- onClick,
381
- quickAdd,
382
- }: {
383
- product: SerializeFrom<Product & WithOkendoStarRatingSnippet>;
384
- label?: string;
385
- className?: string;
386
- loading?: HTMLImageElement['loading'];
387
- onClick?: () => void;
388
- quickAdd?: boolean;
389
- }) {
406
+ products: Promise<{
407
+ products: {
408
+ nodes: (RecommendedProductsQuery['products']['nodes'][0] &
409
+ WithOkendoStarRatingSnippet)[];
410
+ };
411
+ };
390
412
  ```
391
413
 
392
- Add the `OkendoStarRating` component:
414
+ Add `OkendoStarRating` to `RecommendedProducts`:
393
415
 
394
416
  ```tsx
395
417
  <OkendoStarRating
@@ -398,144 +420,137 @@ Add the `OkendoStarRating` component:
398
420
  />
399
421
  ```
400
422
 
401
- For instance, you can add it below the product title, like this:
423
+ For instance, we can add it below the product title, like this:
402
424
 
403
425
  ```tsx
404
- <Text
405
- className="w-full overflow-hidden whitespace-nowrap text-ellipsis "
406
- as="h3"
407
- >
408
- {product.title}
409
- </Text>
426
+ <Image
427
+ data={product.images.nodes[0]}
428
+ aspectRatio="1/1"
429
+ sizes="(min-width: 45em) 20vw, 50vw"
430
+ />
431
+ <h4>{product.title}</h4>
410
432
  <OkendoStarRating
411
433
  productId={product.id}
412
434
  okendoStarRatingSnippet={product.okendoStarRatingSnippet}
413
435
  />
436
+ <small>
437
+ <Money data={product.priceRange.minVariantPrice} />
438
+ </small>
414
439
  ```
415
440
 
416
- ### `app/routes/($lang)/products/$productHandle.tsx`
441
+ We now have the Okendo Star Rating widget visible on our page:
442
+
443
+ ![Okendo's Star Rating widget](./okendo-star-rating-widget.png)
444
+
445
+ ### `app/routes/products.$handle.tsx`
417
446
 
418
- Open `app/routes/($lang)/products/$productHandle.tsx` and add the following imports:
447
+ Add the following imports:
419
448
 
420
449
  ```ts
421
450
  import {
451
+ OKENDO_PRODUCT_REVIEWS_FRAGMENT,
452
+ OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
422
453
  OkendoReviews,
423
454
  OkendoStarRating,
424
- WithOkendoStarRatingSnippet,
425
455
  WithOkendoReviewsSnippet,
426
- OKENDO_PRODUCT_STAR_RATING_FRAGMENT,
427
- OKENDO_PRODUCT_REVIEWS_FRAGMENT,
456
+ WithOkendoStarRatingSnippet,
428
457
  } from "@okendo/shopify-hydrogen";
429
458
  ```
430
459
 
431
- In the storefront query, tweak the type of `product` to be:
432
-
433
- ```ts
434
- product: ProductType & {
435
- selectedVariant?: ProductVariant;
436
- } & WithOkendoStarRatingSnippet &
437
- WithOkendoReviewsSnippet;
438
- ```
439
-
440
- The storefront query should now look like this:
460
+ Locate the `PRODUCT_FRAGMENT` GraphQL query, and append `${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}`, `${OKENDO_PRODUCT_REVIEWS_FRAGMENT}`, `...OkendoStarRatingSnippet`, and `...OkendoReviewsSnippet` to it:
441
461
 
442
462
  ```ts
443
- const { shop, product } = await context.storefront.query<{
444
- product: ProductType & {
445
- selectedVariant?: ProductVariant;
446
- } & WithOkendoStarRatingSnippet &
447
- WithOkendoReviewsSnippet;
448
- shop: Shop;
449
- }>(PRODUCT_QUERY, {
450
- variables: {
451
- handle: productHandle,
452
- selectedOptions,
453
- country: context.storefront.i18n.country,
454
- language: context.storefront.i18n.language,
455
- },
456
- });
463
+ const PRODUCT_FRAGMENT = `#graphql
464
+ ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
465
+ ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
466
+ fragment Product on Product {
467
+ id
468
+ title
469
+ vendor
470
+ handle
471
+ descriptionHtml
472
+ description
473
+ options {
474
+ name
475
+ values
476
+ }
477
+ selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) {
478
+ ...ProductVariant
479
+ }
480
+ variants(first: 1) {
481
+ nodes {
482
+ ...ProductVariant
483
+ }
484
+ }
485
+ seo {
486
+ description
487
+ title
488
+ }
489
+ ...OkendoStarRatingSnippet
490
+ ...OkendoReviewsSnippet
491
+ }
492
+ ${PRODUCT_VARIANT_FRAGMENT}
493
+ ` as const;
457
494
  ```
458
495
 
459
- Add the `OkendoStarRating` component:
496
+ Add `OkendoReviews` to `Product`:
460
497
 
461
498
  ```tsx
462
- <OkendoStarRating
499
+ <OkendoReviews
463
500
  productId={product.id}
464
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
501
+ okendoReviewsSnippet={product.okendoReviewsSnippet}
465
502
  />
466
503
  ```
467
504
 
468
- For instance, you can add it between the product title and the store name, like this:
505
+ For instance, we can add it below the product section, like this:
469
506
 
470
507
  ```tsx
471
- <Heading as="h1" className="whitespace-normal">
472
- {title}
473
- </Heading>
474
- <OkendoStarRating
475
- productId={product.id}
476
- okendoStarRatingSnippet={product.okendoStarRatingSnippet}
477
- />
478
- {vendor && (
479
- <Text className={'opacity-50 font-medium'}>{vendor}</Text>
480
- )}
508
+ <>
509
+ <div className="product">
510
+ <ProductImage image={selectedVariant?.image} />
511
+ <ProductMain
512
+ selectedVariant={selectedVariant}
513
+ product={product}
514
+ variants={variants}
515
+ />
516
+ </div>
517
+
518
+ <OkendoReviews
519
+ productId={product.id}
520
+ okendoReviewsSnippet={product.okendoReviewsSnippet}
521
+ />
522
+ </>
481
523
  ```
482
524
 
483
- Add the `OkendoReviews` component:
525
+ Tweak the type of the `product` prop of `ProductMain`:
484
526
 
485
- ```tsx
486
- <OkendoReviews
487
- productId={product.id}
488
- okendoReviewsSnippet={product.okendoReviewsSnippet}
489
- />
527
+ ```ts
528
+ product: ProductFragment &
529
+ WithOkendoStarRatingSnippet &
530
+ WithOkendoReviewsSnippet;
490
531
  ```
491
532
 
492
- For instance, you can add it below the product section:
533
+ Add `OkendoStarRating` to `ProductMain`:
493
534
 
494
535
  ```tsx
495
- ...
496
- </Section>
497
- <OkendoReviews
536
+ <OkendoStarRating
498
537
  productId={product.id}
499
- okendoReviewsSnippet={product.okendoReviewsSnippet}
538
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
500
539
  />
501
- <Suspense fallback={<Skeleton className="h-32" />}>
502
- ...
503
540
  ```
504
541
 
505
- Locate `PRODUCT_QUERY` and add the following to it:
542
+ For instance, we can add it below the product title, like this:
506
543
 
507
- ```ts
508
- ...
509
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
510
- ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
511
- ...
512
- ...OkendoStarRatingSnippet
513
- ...OkendoReviewsSnippet
544
+ ```tsx
545
+ <div className="product-main">
546
+ <h1>{title}</h1>
547
+ <OkendoStarRating
548
+ productId={product.id}
549
+ okendoStarRatingSnippet={product.okendoStarRatingSnippet}
550
+ />
551
+ <ProductPrice selectedVariant={selectedVariant} />
514
552
  ```
515
553
 
516
- It should now look like this:
554
+ We now have the Okendo Star Rating and Reviews widgets visible on our product page:
517
555
 
518
- ```ts
519
- const PRODUCT_QUERY = `#graphql
520
- ${MEDIA_FRAGMENT}
521
- ${PRODUCT_VARIANT_FRAGMENT}
522
- ${OKENDO_PRODUCT_STAR_RATING_FRAGMENT}
523
- ${OKENDO_PRODUCT_REVIEWS_FRAGMENT}
524
- query Product(
525
- $country: CountryCode
526
- $language: LanguageCode
527
- $handle: String!
528
- $selectedOptions: [SelectedOptionInput!]!
529
- ) @inContext(country: $country, language: $language) {
530
- product(handle: $handle) {
531
- id
532
- title
533
- vendor
534
- handle
535
- descriptionHtml
536
- description
537
- ...OkendoStarRatingSnippet
538
- ...OkendoReviewsSnippet
539
- options {
540
- ...
541
- ```
556
+ ![Okendo's Star Rating and Reviews widgets](./okendo-star-rating-and-reviews-widgets.png)
package/dist/cjs/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var t=require("react");const e=({dataAttributes:e,metafieldContent:n=""})=>{const r=t.useRef(null),s=()=>{r.current&&window.okeWidgetApi.initWidget(r.current)};return t.useEffect((()=>(window.okeWidgetApi&&r.current?s():document.addEventListener("oke-script-loaded",s),()=>{document.removeEventListener("oke-script-loaded",s)})),[e]),t.createElement("div",{ref:r,key:JSON.stringify(e),...e,dangerouslySetInnerHTML:{__html:n}})},n=/^[0-9]*$/;function r(t){if(t)return`shopify-${n.test(t)?t:t.split("/").slice(-1)[0]}`}exports.OKENDO_PRODUCT_REVIEWS_FRAGMENT='#graphql\n\tfragment OkendoReviewsSnippet on Product {\n\t\tokendoReviewsSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "ReviewsWidgetSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n',exports.OKENDO_PRODUCT_STAR_RATING_FRAGMENT='#graphql\n\tfragment OkendoStarRatingSnippet on Product {\n\t\tokendoStarRatingSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "StarRatingSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n',exports.OkendoProvider=({okendoProviderData:e,productUrlFormatter:n})=>{if(!e)return null;const{reviewsHeaderConfig:r,cssVariables:s,customCss:i,initScriptContents:o,preRenderStyleTags:a,starSymbols:d}=e,l="function"==typeof n?n.toString():"string"==typeof n?n:"(product) =>\n\t\t\t\t\tproduct && product.productHandle\n\t\t\t\t\t\t? \"/products/\" + product.productHandle + \"/\" + (product.variantId ? '?variantId=' + product.variantId : '')\n\t\t\t\t\t\t: undefined",c=(s??"").replace('<style id="oke-css-vars">',"").replace("</style>",""),p=i?i.replace('<style id="oke-reviews-custom-css">',"").replace("</style>",""):"";return t.createElement("div",null,t.createElement("script",{id:"oke-reviews-settings",type:"application/json",dangerouslySetInnerHTML:{__html:JSON.stringify(r)}}),t.createElement("style",{id:"oke-css-vars",dangerouslySetInnerHTML:{__html:c}}),p&&t.createElement("style",{id:"oke-reviews-custom-css",dangerouslySetInnerHTML:{__html:p}}),o&&t.createElement("script",{dangerouslySetInnerHTML:{__html:o}}),t.createElement("script",{type:"text/javascript",dangerouslySetInnerHTML:{__html:`window.okeProductUrlFormatter = ${l.toString()}`}}),a&&t.createElement("div",{dangerouslySetInnerHTML:{__html:a}}),d&&t.createElement("div",{dangerouslySetInnerHTML:{__html:d}}))},exports.OkendoReviews=({productId:n,okendoReviewsSnippet:s})=>{const i={"data-oke-widget":"","data-oke-reviews-product-id":r(n)};return t.createElement(e,{dataAttributes:i,metafieldContent:s?.value})},exports.OkendoStarRating=({productId:n,okendoStarRatingSnippet:s})=>{const i={"data-oke-star-rating":"","data-oke-reviews-product-id":r(n)};return t.createElement(e,{dataAttributes:i,metafieldContent:s?.value})},exports.getOkendoProviderData=async({context:t,subscriberId:e,apiDomain:n,cdnDomain:r})=>{const s=`https://${n||"api.okendo.io/v1"}/stores/${e}/widget_plus_settings`,i=await fetch(s);if(!i.ok)return console.error(`Failed to retrieve subscriber settings for subscriber ID '${e}'.`),null;const{reviewsHeaderConfig:o,cssVariables:a,customCss:d,starSymbols:l}=await i.json(),c=await fetch(`https://${r||"cdn-static.okendo.io"}/reviews-widget-plus/js/okendo-reviews.js`);if(!c.ok)return console.error("Failed to retrieve widget initialisation script."),null;const p=await c.text(),{shop:{widgetPreRenderStyleTags:u}}=await t.storefront.query('#graphql\n\t\tquery metafields {\n\t\t\tshop {\n\t\t\t\twidgetPreRenderStyleTags: metafield(\n\t\t\t\t\tnamespace: "okendo"\n\t\t\t\t\tkey: "WidgetPreRenderStyleTags"\n\t\t\t\t) {\n\t\t\t\t\tvalue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t'),g=u?.value??"";return g||console.warn("Failed to retrieve pre-rendered widget style settings."),{reviewsHeaderConfig:o,cssVariables:a,customCss:d,initScriptContents:p,preRenderStyleTags:g,starSymbols:l}};
1
+ "use strict";var t=require("react");const e=({dataAttributes:e,metafieldContent:n=""})=>{const r=t.useRef(null),o=()=>{r.current&&window.okeWidgetApi.initWidget(r.current)};return t.useEffect((()=>(window.okeWidgetApi&&r.current?o():document.addEventListener("oke-script-loaded",o),()=>{document.removeEventListener("oke-script-loaded",o)})),[e]),t.createElement("div",{ref:r,key:JSON.stringify(e),...e,dangerouslySetInnerHTML:n?{__html:n}:void 0})},n=/^[0-9]*$/;function r(t){if(t)return`shopify-${n.test(t)?t:t.split("/").slice(-1)[0]}`}exports.OKENDO_PRODUCT_REVIEWS_FRAGMENT='#graphql\n\tfragment OkendoReviewsSnippet on Product {\n\t\tokendoReviewsSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "ReviewsWidgetSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n',exports.OKENDO_PRODUCT_STAR_RATING_FRAGMENT='#graphql\n\tfragment OkendoStarRatingSnippet on Product {\n\t\tokendoStarRatingSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "StarRatingSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n',exports.OkendoProvider=({nonce:e="",okendoProviderData:n,productUrlFormatter:r})=>{if(!n)return null;const{reviewsHeaderConfig:o,cssVariables:s,customCss:i,initScriptContents:a,preRenderStyleTags:d,starSymbols:c}=n,l="function"==typeof r?r.toString():"string"==typeof r?r:"(product) =>\n\t\t\t\t\tproduct && product.productHandle\n\t\t\t\t\t\t? \"/products/\" + product.productHandle + \"/\" + (product.variantId ? '?variantId=' + product.variantId : '')\n\t\t\t\t\t\t: undefined",p=(s??"").replace('<style id="oke-css-vars">',"").replace("</style>",""),u=i?i.replace('<style id="oke-reviews-custom-css">',"").replace("</style>",""):"";return t.createElement(t.Fragment,null,t.createElement("script",{nonce:e,id:"oke-reviews-settings",type:"application/json",dangerouslySetInnerHTML:{__html:JSON.stringify(o)}}),t.createElement("style",{nonce:e,id:"oke-css-vars",dangerouslySetInnerHTML:{__html:p}}),u&&t.createElement("style",{nonce:e,id:"oke-reviews-custom-css",dangerouslySetInnerHTML:{__html:u}}),a&&t.createElement("script",{nonce:e,dangerouslySetInnerHTML:{__html:a}}),t.createElement("script",{nonce:e,type:"text/javascript",dangerouslySetInnerHTML:{__html:`window.okeProductUrlFormatter = ${l.toString()}`}}),d&&t.createElement("div",{dangerouslySetInnerHTML:{__html:d}}),c&&t.createElement("div",{dangerouslySetInnerHTML:{__html:c}}))},exports.OkendoReviews=({productId:n,okendoReviewsSnippet:o})=>{const s={"data-oke-widget":"","data-oke-reviews-product-id":r(n)};return t.createElement(e,{dataAttributes:s,metafieldContent:o?.value})},exports.OkendoStarRating=({productId:n,okendoStarRatingSnippet:o})=>{const s={"data-oke-star-rating":"","data-oke-reviews-product-id":r(n)};return t.createElement(e,{dataAttributes:s,metafieldContent:o?.value})},exports.getOkendoProviderData=async({context:t,subscriberId:e,apiDomain:n,cdnDomain:r})=>{const o=`https://${n||"api.okendo.io/v1"}/stores/${e}/widget_plus_settings`,s=await fetch(o);if(!s.ok)return console.error(`Failed to retrieve subscriber settings for subscriber ID '${e}'.`),null;const{reviewsHeaderConfig:i,cssVariables:a,customCss:d,starSymbols:c}=await s.json(),l=await fetch(`https://${r||"cdn-static.okendo.io"}/reviews-widget-plus/js/okendo-reviews.js`);if(!l.ok)return console.error("Failed to retrieve widget initialisation script."),null;const p=await l.text(),{shop:{widgetPreRenderStyleTags:u}}=await t.storefront.query('#graphql\n\t\tquery metafields {\n\t\t\tshop {\n\t\t\t\twidgetPreRenderStyleTags: metafield(\n\t\t\t\t\tnamespace: "okendo"\n\t\t\t\t\tkey: "WidgetPreRenderStyleTags"\n\t\t\t\t) {\n\t\t\t\t\tvalue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t'),g=u?.value??"";return g||console.warn("Failed to retrieve pre-rendered widget style settings."),{reviewsHeaderConfig:i,cssVariables:a,customCss:d,initScriptContents:p,preRenderStyleTags:g,starSymbols:c}};
@@ -29,7 +29,14 @@ interface ReviewProduct {
29
29
  variantId?: string;
30
30
  }
31
31
  interface OkendoProviderProps {
32
+ /**
33
+ * A nonce that will be added to Okendo's scripts;
34
+ * this is required if Content Security Policy is set up on your store.
35
+ */
36
+ nonce?: string;
37
+ /** The data returned by `getOkendoProviderData` */
32
38
  okendoProviderData: Partial<OkendoProviderData> | null;
39
+ /** An optional product URL formatter */
33
40
  productUrlFormatter?: (product: ReviewProduct) => string;
34
41
  }
35
42
  export declare const OkendoProvider: FC<OkendoProviderProps>;
package/dist/esm/index.js CHANGED
@@ -1 +1 @@
1
- import t,{useRef as e,useEffect as n}from"react";const r=async({context:t,subscriberId:e,apiDomain:n,cdnDomain:r})=>{const i=`https://${n||"api.okendo.io/v1"}/stores/${e}/widget_plus_settings`,o=await fetch(i);if(!o.ok)return console.error(`Failed to retrieve subscriber settings for subscriber ID '${e}'.`),null;const{reviewsHeaderConfig:s,cssVariables:a,customCss:d,starSymbols:l}=await o.json(),c=await fetch(`https://${r||"cdn-static.okendo.io"}/reviews-widget-plus/js/okendo-reviews.js`);if(!c.ok)return console.error("Failed to retrieve widget initialisation script."),null;const p=await c.text(),{shop:{widgetPreRenderStyleTags:u}}=await t.storefront.query('#graphql\n\t\tquery metafields {\n\t\t\tshop {\n\t\t\t\twidgetPreRenderStyleTags: metafield(\n\t\t\t\t\tnamespace: "okendo"\n\t\t\t\t\tkey: "WidgetPreRenderStyleTags"\n\t\t\t\t) {\n\t\t\t\t\tvalue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t'),m=u?.value??"";return m||console.warn("Failed to retrieve pre-rendered widget style settings."),{reviewsHeaderConfig:s,cssVariables:a,customCss:d,initScriptContents:p,preRenderStyleTags:m,starSymbols:l}},i=({okendoProviderData:e,productUrlFormatter:n})=>{if(!e)return null;const{reviewsHeaderConfig:r,cssVariables:i,customCss:o,initScriptContents:s,preRenderStyleTags:a,starSymbols:d}=e,l="function"==typeof n?n.toString():"string"==typeof n?n:"(product) =>\n\t\t\t\t\tproduct && product.productHandle\n\t\t\t\t\t\t? \"/products/\" + product.productHandle + \"/\" + (product.variantId ? '?variantId=' + product.variantId : '')\n\t\t\t\t\t\t: undefined",c=(i??"").replace('<style id="oke-css-vars">',"").replace("</style>",""),p=o?o.replace('<style id="oke-reviews-custom-css">',"").replace("</style>",""):"";return t.createElement("div",null,t.createElement("script",{id:"oke-reviews-settings",type:"application/json",dangerouslySetInnerHTML:{__html:JSON.stringify(r)}}),t.createElement("style",{id:"oke-css-vars",dangerouslySetInnerHTML:{__html:c}}),p&&t.createElement("style",{id:"oke-reviews-custom-css",dangerouslySetInnerHTML:{__html:p}}),s&&t.createElement("script",{dangerouslySetInnerHTML:{__html:s}}),t.createElement("script",{type:"text/javascript",dangerouslySetInnerHTML:{__html:`window.okeProductUrlFormatter = ${l.toString()}`}}),a&&t.createElement("div",{dangerouslySetInnerHTML:{__html:a}}),d&&t.createElement("div",{dangerouslySetInnerHTML:{__html:d}}))},o=({dataAttributes:r,metafieldContent:i=""})=>{const o=e(null),s=()=>{o.current&&window.okeWidgetApi.initWidget(o.current)};return n((()=>(window.okeWidgetApi&&o.current?s():document.addEventListener("oke-script-loaded",s),()=>{document.removeEventListener("oke-script-loaded",s)})),[r]),t.createElement("div",{ref:o,key:JSON.stringify(r),...r,dangerouslySetInnerHTML:{__html:i}})},s=/^[0-9]*$/;function a(t){if(t)return`shopify-${s.test(t)?t:t.split("/").slice(-1)[0]}`}const d=({productId:e,okendoReviewsSnippet:n})=>{const r={"data-oke-widget":"","data-oke-reviews-product-id":a(e)};return t.createElement(o,{dataAttributes:r,metafieldContent:n?.value})},l=({productId:e,okendoStarRatingSnippet:n})=>{const r={"data-oke-star-rating":"","data-oke-reviews-product-id":a(e)};return t.createElement(o,{dataAttributes:r,metafieldContent:n?.value})},c='#graphql\n\tfragment OkendoStarRatingSnippet on Product {\n\t\tokendoStarRatingSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "StarRatingSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n',p='#graphql\n\tfragment OkendoReviewsSnippet on Product {\n\t\tokendoReviewsSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "ReviewsWidgetSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n';export{p as OKENDO_PRODUCT_REVIEWS_FRAGMENT,c as OKENDO_PRODUCT_STAR_RATING_FRAGMENT,i as OkendoProvider,d as OkendoReviews,l as OkendoStarRating,r as getOkendoProviderData};
1
+ import t,{useRef as e,useEffect as n}from"react";const r=async({context:t,subscriberId:e,apiDomain:n,cdnDomain:r})=>{const o=`https://${n||"api.okendo.io/v1"}/stores/${e}/widget_plus_settings`,i=await fetch(o);if(!i.ok)return console.error(`Failed to retrieve subscriber settings for subscriber ID '${e}'.`),null;const{reviewsHeaderConfig:s,cssVariables:a,customCss:d,starSymbols:c}=await i.json(),l=await fetch(`https://${r||"cdn-static.okendo.io"}/reviews-widget-plus/js/okendo-reviews.js`);if(!l.ok)return console.error("Failed to retrieve widget initialisation script."),null;const p=await l.text(),{shop:{widgetPreRenderStyleTags:u}}=await t.storefront.query('#graphql\n\t\tquery metafields {\n\t\t\tshop {\n\t\t\t\twidgetPreRenderStyleTags: metafield(\n\t\t\t\t\tnamespace: "okendo"\n\t\t\t\t\tkey: "WidgetPreRenderStyleTags"\n\t\t\t\t) {\n\t\t\t\t\tvalue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t'),m=u?.value??"";return m||console.warn("Failed to retrieve pre-rendered widget style settings."),{reviewsHeaderConfig:s,cssVariables:a,customCss:d,initScriptContents:p,preRenderStyleTags:m,starSymbols:c}},o=({nonce:e="",okendoProviderData:n,productUrlFormatter:r})=>{if(!n)return null;const{reviewsHeaderConfig:o,cssVariables:i,customCss:s,initScriptContents:a,preRenderStyleTags:d,starSymbols:c}=n,l="function"==typeof r?r.toString():"string"==typeof r?r:"(product) =>\n\t\t\t\t\tproduct && product.productHandle\n\t\t\t\t\t\t? \"/products/\" + product.productHandle + \"/\" + (product.variantId ? '?variantId=' + product.variantId : '')\n\t\t\t\t\t\t: undefined",p=(i??"").replace('<style id="oke-css-vars">',"").replace("</style>",""),u=s?s.replace('<style id="oke-reviews-custom-css">',"").replace("</style>",""):"";return t.createElement(t.Fragment,null,t.createElement("script",{nonce:e,id:"oke-reviews-settings",type:"application/json",dangerouslySetInnerHTML:{__html:JSON.stringify(o)}}),t.createElement("style",{nonce:e,id:"oke-css-vars",dangerouslySetInnerHTML:{__html:p}}),u&&t.createElement("style",{nonce:e,id:"oke-reviews-custom-css",dangerouslySetInnerHTML:{__html:u}}),a&&t.createElement("script",{nonce:e,dangerouslySetInnerHTML:{__html:a}}),t.createElement("script",{nonce:e,type:"text/javascript",dangerouslySetInnerHTML:{__html:`window.okeProductUrlFormatter = ${l.toString()}`}}),d&&t.createElement("div",{dangerouslySetInnerHTML:{__html:d}}),c&&t.createElement("div",{dangerouslySetInnerHTML:{__html:c}}))},i=({dataAttributes:r,metafieldContent:o=""})=>{const i=e(null),s=()=>{i.current&&window.okeWidgetApi.initWidget(i.current)};return n((()=>(window.okeWidgetApi&&i.current?s():document.addEventListener("oke-script-loaded",s),()=>{document.removeEventListener("oke-script-loaded",s)})),[r]),t.createElement("div",{ref:i,key:JSON.stringify(r),...r,dangerouslySetInnerHTML:o?{__html:o}:void 0})},s=/^[0-9]*$/;function a(t){if(t)return`shopify-${s.test(t)?t:t.split("/").slice(-1)[0]}`}const d=({productId:e,okendoReviewsSnippet:n})=>{const r={"data-oke-widget":"","data-oke-reviews-product-id":a(e)};return t.createElement(i,{dataAttributes:r,metafieldContent:n?.value})},c=({productId:e,okendoStarRatingSnippet:n})=>{const r={"data-oke-star-rating":"","data-oke-reviews-product-id":a(e)};return t.createElement(i,{dataAttributes:r,metafieldContent:n?.value})},l='#graphql\n\tfragment OkendoStarRatingSnippet on Product {\n\t\tokendoStarRatingSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "StarRatingSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n',p='#graphql\n\tfragment OkendoReviewsSnippet on Product {\n\t\tokendoReviewsSnippet: metafield(\n\t\t\tnamespace: "okendo"\n\t\t\tkey: "ReviewsWidgetSnippet"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n';export{p as OKENDO_PRODUCT_REVIEWS_FRAGMENT,l as OKENDO_PRODUCT_STAR_RATING_FRAGMENT,o as OkendoProvider,d as OkendoReviews,c as OkendoStarRating,r as getOkendoProviderData};
@@ -29,7 +29,14 @@ interface ReviewProduct {
29
29
  variantId?: string;
30
30
  }
31
31
  interface OkendoProviderProps {
32
+ /**
33
+ * A nonce that will be added to Okendo's scripts;
34
+ * this is required if Content Security Policy is set up on your store.
35
+ */
36
+ nonce?: string;
37
+ /** The data returned by `getOkendoProviderData` */
32
38
  okendoProviderData: Partial<OkendoProviderData> | null;
39
+ /** An optional product URL formatter */
33
40
  productUrlFormatter?: (product: ReviewProduct) => string;
34
41
  }
35
42
  export declare const OkendoProvider: FC<OkendoProviderProps>;
package/dist/index.d.ts CHANGED
@@ -31,7 +31,14 @@ interface ReviewProduct {
31
31
  variantId?: string;
32
32
  }
33
33
  interface OkendoProviderProps {
34
+ /**
35
+ * A nonce that will be added to Okendo's scripts;
36
+ * this is required if Content Security Policy is set up on your store.
37
+ */
38
+ nonce?: string;
39
+ /** The data returned by `getOkendoProviderData` */
34
40
  okendoProviderData: Partial<OkendoProviderData> | null;
41
+ /** An optional product URL formatter */
35
42
  productUrlFormatter?: (product: ReviewProduct) => string;
36
43
  }
37
44
  declare const OkendoProvider: FC<OkendoProviderProps>;
@@ -61,4 +68,4 @@ declare const OkendoStarRating: FC<OkendoStarRatingProps>;
61
68
  declare const OKENDO_PRODUCT_STAR_RATING_FRAGMENT = "#graphql\n\tfragment OkendoStarRatingSnippet on Product {\n\t\tokendoStarRatingSnippet: metafield(\n\t\t\tnamespace: \"okendo\"\n\t\t\tkey: \"StarRatingSnippet\"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n";
62
69
  declare const OKENDO_PRODUCT_REVIEWS_FRAGMENT = "#graphql\n\tfragment OkendoReviewsSnippet on Product {\n\t\tokendoReviewsSnippet: metafield(\n\t\t\tnamespace: \"okendo\"\n\t\t\tkey: \"ReviewsWidgetSnippet\"\n\t\t) {\n\t\t\tvalue\n\t\t}\n\t}\n";
63
70
 
64
- export { OKENDO_PRODUCT_REVIEWS_FRAGMENT, OKENDO_PRODUCT_STAR_RATING_FRAGMENT, OkendoProvider, OkendoReviews, OkendoStarRating, WithOkendoReviewsSnippet, WithOkendoStarRatingSnippet, getOkendoProviderData };
71
+ export { OKENDO_PRODUCT_REVIEWS_FRAGMENT, OKENDO_PRODUCT_STAR_RATING_FRAGMENT, OkendoProvider, OkendoReviews, OkendoStarRating, type WithOkendoReviewsSnippet, type WithOkendoStarRatingSnippet, getOkendoProviderData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okendo/shopify-hydrogen",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "Okendo React components for Shopify Hydrogen 2 (Remix)",
5
5
  "author": "Okendo",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -15,32 +15,33 @@
15
15
  "types": "dist/index.d.ts",
16
16
  "scripts": {
17
17
  "build": "npm run eslint -- --max-warnings=0 && rimraf dist && rollup -c",
18
+ "prepack": "npm run build",
18
19
  "eslint": "eslint src"
19
20
  },
20
21
  "devDependencies": {
21
- "@okendo/reviews-common": "^5.38.0",
22
- "@okendo/reviews-widget-plus": "^0.67.0",
22
+ "@okendo/reviews-common": "^5.163.1",
23
+ "@okendo/reviews-widget-plus": "^0.68.3",
23
24
  "@okendo/tsconfig": "0.0.2",
24
- "@rollup/plugin-commonjs": "^24.1.0",
25
- "@rollup/plugin-node-resolve": "^15.0.2",
26
- "@rollup/plugin-terser": "^0.4.1",
27
- "@rollup/plugin-typescript": "^11.1.0",
28
- "@types/react": "^18.0.20",
29
- "@types/react-dom": "^18.0.6",
30
- "@typescript-eslint/eslint-plugin": "^5.58.0",
31
- "@typescript-eslint/parser": "^5.58.0",
32
- "eslint": "^8.20.0",
33
- "eslint-plugin-react": "^7.32.2",
25
+ "@rollup/plugin-commonjs": "^25.0.7",
26
+ "@rollup/plugin-node-resolve": "^15.2.3",
27
+ "@rollup/plugin-terser": "^0.4.4",
28
+ "@rollup/plugin-typescript": "^11.1.5",
29
+ "@types/react": "^18.2.45",
30
+ "@types/react-dom": "^18.2.18",
31
+ "@typescript-eslint/eslint-plugin": "^6.15.0",
32
+ "@typescript-eslint/parser": "^6.15.0",
33
+ "eslint": "^8.56.0",
34
+ "eslint-plugin-react": "^7.33.2",
34
35
  "eslint-plugin-react-hooks": "^4.6.0",
35
- "rimraf": "^5.0.0",
36
- "rollup": "^3.20.2",
37
- "rollup-plugin-dts": "^5.3.0",
36
+ "rimraf": "^5.0.5",
37
+ "rollup": "^4.9.1",
38
+ "rollup-plugin-dts": "^6.1.0",
38
39
  "rollup-plugin-peer-deps-external": "^2.2.4",
39
- "typescript": "^4.9.5"
40
+ "typescript": "^5.3.3"
40
41
  },
41
42
  "peerDependencies": {
42
- "@shopify/hydrogen": "^2023.1.6",
43
- "@shopify/remix-oxygen": "^1.0.4",
43
+ "@shopify/hydrogen": "*",
44
+ "@shopify/remix-oxygen": "*",
44
45
  "react": "^18.2.0"
45
46
  }
46
47
  }