@revova/hydrogen 1.0.0 → 1.0.2

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/dist/index.js CHANGED
@@ -3,8 +3,25 @@ import { useState as useState4 } from "react";
3
3
 
4
4
  // src/hooks/useReviews.ts
5
5
  import { useEffect, useState, useCallback } from "react";
6
+
7
+ // src/utils.ts
8
+ function apiFetch({ apiUrl, shop, apiToken }, path, params, init) {
9
+ const searchParams = new URLSearchParams({ shop, ...params });
10
+ return fetch(`${apiUrl}${path}?${searchParams}`, {
11
+ ...init,
12
+ headers: {
13
+ Authorization: `Bearer ${apiToken}`,
14
+ "Content-Type": "application/json",
15
+ ...init?.headers ?? {}
16
+ }
17
+ });
18
+ }
19
+
20
+ // src/hooks/useReviews.ts
6
21
  function useReviews({
7
- proxyUrl,
22
+ apiUrl,
23
+ shop,
24
+ apiToken,
8
25
  productId,
9
26
  page: initialPage = 1,
10
27
  limit = 10,
@@ -13,24 +30,20 @@ function useReviews({
13
30
  }) {
14
31
  const [page, setPage] = useState(initialPage);
15
32
  const [sort, setSort] = useState(initialSort);
16
- const [state, setState] = useState({
17
- data: null,
18
- loading: true,
19
- error: null
20
- });
33
+ const [state, setState] = useState({ data: null, loading: true, error: null });
21
34
  const [tick, setTick] = useState(0);
22
35
  const refetch = useCallback(() => setTick((t) => t + 1), []);
23
36
  useEffect(() => {
24
37
  let cancelled = false;
25
38
  setState((s) => ({ ...s, loading: true, error: null }));
26
- const params = new URLSearchParams({
39
+ const params = {
27
40
  productId,
28
41
  page: String(page),
29
42
  limit: String(limit),
30
43
  sort,
31
44
  ...locale ? { locale } : {}
32
- });
33
- fetch(`${proxyUrl}/reviews?${params.toString()}`).then((res) => {
45
+ };
46
+ apiFetch({ apiUrl, shop, apiToken }, "/api/reviews", params).then((res) => {
34
47
  if (!res.ok) throw new Error(`Revova: reviews fetch failed (${res.status})`);
35
48
  return res.json();
36
49
  }).then((data) => {
@@ -42,15 +55,8 @@ function useReviews({
42
55
  return () => {
43
56
  cancelled = true;
44
57
  };
45
- }, [proxyUrl, productId, page, limit, sort, locale, tick]);
46
- return {
47
- ...state,
48
- refetch,
49
- setPage,
50
- setSort,
51
- currentPage: page,
52
- currentSort: sort
53
- };
58
+ }, [apiUrl, shop, apiToken, productId, page, limit, sort, locale, tick]);
59
+ return { ...state, refetch, setPage, setSort, currentPage: page, currentSort: sort };
54
60
  }
55
61
 
56
62
  // src/components/StarRating.tsx
@@ -101,44 +107,28 @@ import { useState as useState3 } from "react";
101
107
 
102
108
  // src/hooks/useSubmitReview.ts
103
109
  import { useState as useState2, useCallback as useCallback2 } from "react";
104
- var INITIAL_STATE = {
105
- submitting: false,
106
- success: false,
107
- error: null,
108
- result: null
109
- };
110
- function useSubmitReview(proxyUrl) {
110
+ var INITIAL_STATE = { submitting: false, success: false, error: null, result: null };
111
+ function useSubmitReview(creds) {
111
112
  const [state, setState] = useState2(INITIAL_STATE);
112
113
  const submit = useCallback2(
113
114
  async (payload) => {
114
115
  setState({ submitting: true, success: false, error: null, result: null });
115
116
  try {
116
- const res = await fetch(`${proxyUrl}/reviews`, {
117
+ const res = await apiFetch(creds, "/api/reviews", {}, {
117
118
  method: "POST",
118
- headers: { "Content-Type": "application/json" },
119
119
  body: JSON.stringify(payload)
120
120
  });
121
121
  const json = await res.json();
122
122
  if (!res.ok) {
123
- setState({
124
- submitting: false,
125
- success: false,
126
- error: json.error ?? `Submission failed (${res.status})`,
127
- result: null
128
- });
123
+ setState({ submitting: false, success: false, error: json.error ?? `Submission failed (${res.status})`, result: null });
129
124
  return;
130
125
  }
131
126
  setState({ submitting: false, success: true, error: null, result: json });
132
127
  } catch (err) {
133
- setState({
134
- submitting: false,
135
- success: false,
136
- error: err instanceof Error ? err.message : "An unexpected error occurred.",
137
- result: null
138
- });
128
+ setState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An unexpected error occurred.", result: null });
139
129
  }
140
130
  },
141
- [proxyUrl]
131
+ [creds]
142
132
  );
143
133
  const reset = useCallback2(() => setState(INITIAL_STATE), []);
144
134
  return { ...state, submit, reset };
@@ -426,8 +416,8 @@ function FieldRenderer(props) {
426
416
  return null;
427
417
  }
428
418
  }
429
- function ReviewForm({ proxyUrl, productId, form, onSuccess, className }) {
430
- const { submit, submitting, success, error, result } = useSubmitReview(proxyUrl);
419
+ function ReviewForm({ apiUrl, shop, apiToken, productId, form, onSuccess, className }) {
420
+ const { submit, submitting, success, error, result } = useSubmitReview({ apiUrl, shop, apiToken });
431
421
  const [answers, setAnswers] = useState3({});
432
422
  const [email, setEmail] = useState3("");
433
423
  const starField = form.fields.find((f) => f.type === "STAR_RATING");
@@ -534,7 +524,9 @@ function ReviewForm({ proxyUrl, productId, form, onSuccess, className }) {
534
524
  // src/components/ReviewWidget.tsx
535
525
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
536
526
  function ReviewWidget({
537
- proxyUrl,
527
+ apiUrl,
528
+ shop,
529
+ apiToken,
538
530
  productId,
539
531
  locale,
540
532
  pageSize = 10,
@@ -542,9 +534,10 @@ function ReviewWidget({
542
534
  starColor,
543
535
  className
544
536
  }) {
537
+ const creds = { apiUrl, shop, apiToken };
545
538
  const [showingForm, setShowingForm] = useState4(false);
546
539
  const { data, loading, error, setPage, setSort, currentPage, currentSort } = useReviews({
547
- proxyUrl,
540
+ ...creds,
548
541
  productId,
549
542
  limit: pageSize,
550
543
  ...locale !== void 0 ? { locale } : {}
@@ -579,7 +572,7 @@ function ReviewWidget({
579
572
  showingForm && form && /* @__PURE__ */ jsx3("div", { style: { marginBottom: 24 }, children: /* @__PURE__ */ jsx3(
580
573
  ReviewForm,
581
574
  {
582
- proxyUrl,
575
+ ...creds,
583
576
  productId,
584
577
  form,
585
578
  onSuccess: () => setShowingForm(false)
@@ -635,16 +628,11 @@ function ReviewWidget({
635
628
 
636
629
  // src/hooks/useWidgetGlobals.ts
637
630
  import { useEffect as useEffect2, useState as useState5 } from "react";
638
- function useWidgetGlobals({ proxyUrl, limit = 20 }) {
639
- const [state, setState] = useState5({
640
- data: null,
641
- loading: true,
642
- error: null
643
- });
631
+ function useWidgetGlobals({ apiUrl, shop, apiToken, limit = 20 }) {
632
+ const [state, setState] = useState5({ data: null, loading: true, error: null });
644
633
  useEffect2(() => {
645
634
  let cancelled = false;
646
- const params = new URLSearchParams({ limit: String(limit) });
647
- fetch(`${proxyUrl}/widget-globals?${params.toString()}`).then((res) => {
635
+ apiFetch({ apiUrl, shop, apiToken }, "/api/widget-globals", { limit: String(limit) }).then((res) => {
648
636
  if (!res.ok) throw new Error(`Revova: widget-globals fetch failed (${res.status})`);
649
637
  return res.json();
650
638
  }).then((data) => {
@@ -656,14 +644,14 @@ function useWidgetGlobals({ proxyUrl, limit = 20 }) {
656
644
  return () => {
657
645
  cancelled = true;
658
646
  };
659
- }, [proxyUrl, limit]);
647
+ }, [apiUrl, shop, apiToken, limit]);
660
648
  return state;
661
649
  }
662
650
 
663
651
  // src/components/ReviewCount.tsx
664
652
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
665
- function ReviewCount({ proxyUrl, starColor, starSize, className }) {
666
- const { data, loading } = useWidgetGlobals({ proxyUrl });
653
+ function ReviewCount({ apiUrl, shop, apiToken, starColor, starSize, className }) {
654
+ const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken });
667
655
  if (loading || !data?.stats?.averageRating) return null;
668
656
  const avg = parseFloat(data.stats.averageRating);
669
657
  return /* @__PURE__ */ jsxs4("span", { className, style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
@@ -681,14 +669,16 @@ function ReviewCount({ proxyUrl, starColor, starSize, className }) {
681
669
  import React3, { useState as useState6 } from "react";
682
670
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
683
671
  function ReviewCarousel({
684
- proxyUrl,
672
+ apiUrl,
673
+ shop,
674
+ apiToken,
685
675
  limit = 10,
686
676
  autoPlay = true,
687
677
  intervalMs = 4e3,
688
678
  starColor,
689
679
  className
690
680
  }) {
691
- const { data, loading } = useWidgetGlobals({ proxyUrl, limit });
681
+ const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
692
682
  const [index, setIndex] = useState6(0);
693
683
  const reviews = data?.reviews ?? [];
694
684
  React3.useEffect(() => {
@@ -736,13 +726,15 @@ function ReviewCarousel({
736
726
  import { useState as useState7 } from "react";
737
727
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
738
728
  function ReviewGallery({
739
- proxyUrl,
729
+ apiUrl,
730
+ shop,
731
+ apiToken,
740
732
  limit = 20,
741
733
  columns = 3,
742
734
  starColor,
743
735
  className
744
736
  }) {
745
- const { data, loading } = useWidgetGlobals({ proxyUrl, limit });
737
+ const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
746
738
  const [lightbox, setLightbox] = useState7(null);
747
739
  const items = (data?.reviews ?? []).filter((r) => r.image).map((r) => ({ url: r.image, review: r })).slice(0, limit);
748
740
  if (loading) return /* @__PURE__ */ jsx6("div", { className, children: "Loading gallery\u2026" });
@@ -859,7 +851,8 @@ import { useState as useState9 } from "react";
859
851
  // src/hooks/useQnA.ts
860
852
  import { useEffect as useEffect3, useState as useState8, useCallback as useCallback3 } from "react";
861
853
  var INITIAL_SUBMIT = { submitting: false, success: false, error: null };
862
- function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" }) {
854
+ function useQnA({ apiUrl, shop, apiToken, productId, page: initialPage = 1, sort = "recent" }) {
855
+ const creds = { apiUrl, shop, apiToken };
863
856
  const [page, setPage] = useState8(initialPage);
864
857
  const [tick, setTick] = useState8(0);
865
858
  const [state, setState] = useState8({ data: null, loading: true, error: null });
@@ -868,8 +861,7 @@ function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" })
868
861
  useEffect3(() => {
869
862
  let cancelled = false;
870
863
  setState((s) => ({ ...s, loading: true, error: null }));
871
- const params = new URLSearchParams({ productId, page: String(page), sort });
872
- fetch(`${proxyUrl}/qna?${params.toString()}`).then((res) => {
864
+ apiFetch(creds, "/api/qna", { productId, page: String(page), sort }).then((res) => {
873
865
  if (!res.ok) throw new Error(`Revova: qna fetch failed (${res.status})`);
874
866
  return res.json();
875
867
  }).then((data) => {
@@ -881,15 +873,11 @@ function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" })
881
873
  return () => {
882
874
  cancelled = true;
883
875
  };
884
- }, [proxyUrl, productId, page, sort, tick]);
885
- const submitQuestion = useCallback3(async (payload) => {
876
+ }, [apiUrl, shop, apiToken, productId, page, sort, tick]);
877
+ const postQnA = useCallback3(async (payload) => {
886
878
  setSubmitState({ submitting: true, success: false, error: null });
887
879
  try {
888
- const res = await fetch(`${proxyUrl}/qna`, {
889
- method: "POST",
890
- headers: { "Content-Type": "application/json" },
891
- body: JSON.stringify(payload)
892
- });
880
+ const res = await apiFetch(creds, "/api/qna", {}, { method: "POST", body: JSON.stringify(payload) });
893
881
  const json = await res.json();
894
882
  if (!res.ok) {
895
883
  setSubmitState({ submitting: false, success: false, error: json.error ?? `Failed (${res.status})` });
@@ -900,35 +888,27 @@ function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" })
900
888
  } catch (err) {
901
889
  setSubmitState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An error occurred." });
902
890
  }
903
- }, [proxyUrl, refetch]);
904
- const submitAnswer = useCallback3(async (payload) => {
905
- setSubmitState({ submitting: true, success: false, error: null });
906
- try {
907
- const res = await fetch(`${proxyUrl}/qna`, {
908
- method: "POST",
909
- headers: { "Content-Type": "application/json" },
910
- body: JSON.stringify(payload)
911
- });
912
- const json = await res.json();
913
- if (!res.ok) {
914
- setSubmitState({ submitting: false, success: false, error: json.error ?? `Failed (${res.status})` });
915
- return;
916
- }
917
- setSubmitState({ submitting: false, success: true, error: null });
918
- refetch();
919
- } catch (err) {
920
- setSubmitState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An error occurred." });
921
- }
922
- }, [proxyUrl, refetch]);
891
+ }, [apiUrl, shop, apiToken, refetch]);
923
892
  const resetSubmit = useCallback3(() => setSubmitState(INITIAL_SUBMIT), []);
924
- return { ...state, setPage, currentPage: page, refetch, submitQuestion, submitAnswer, submitState, resetSubmit };
893
+ return {
894
+ ...state,
895
+ setPage,
896
+ currentPage: page,
897
+ refetch,
898
+ submitQuestion: postQnA,
899
+ submitAnswer: postQnA,
900
+ submitState,
901
+ resetSubmit
902
+ };
925
903
  }
926
904
 
927
905
  // src/components/QnAWidget.tsx
928
906
  import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
929
- function QnAWidget({ proxyUrl, productId, className }) {
907
+ function QnAWidget({ apiUrl, shop, apiToken, productId, className }) {
930
908
  const { data, loading, error, setPage, currentPage, submitQuestion, submitState, resetSubmit } = useQnA({
931
- proxyUrl,
909
+ apiUrl,
910
+ shop,
911
+ apiToken,
932
912
  productId
933
913
  });
934
914
  const [showForm, setShowForm] = useState9(false);
@@ -1020,14 +1000,16 @@ function QnAWidget({ proxyUrl, productId, className }) {
1020
1000
  import { useEffect as useEffect4, useState as useState10 } from "react";
1021
1001
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1022
1002
  function SocialProofPopup({
1023
- proxyUrl,
1003
+ apiUrl,
1004
+ shop,
1005
+ apiToken,
1024
1006
  position = "bottom-left",
1025
1007
  intervalMs = 8e3,
1026
1008
  displayMs = 5e3,
1027
1009
  starColor,
1028
1010
  className
1029
1011
  }) {
1030
- const { data } = useWidgetGlobals({ proxyUrl });
1012
+ const { data } = useWidgetGlobals({ apiUrl, shop, apiToken });
1031
1013
  const [current, setCurrent] = useState10(null);
1032
1014
  const [visible, setVisible] = useState10(false);
1033
1015
  const [dismissed, setDismissed] = useState10(false);
@@ -1129,13 +1111,15 @@ function SocialProofPopup({
1129
1111
  import { useRef } from "react";
1130
1112
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1131
1113
  function ReviewTicker({
1132
- proxyUrl,
1114
+ apiUrl,
1115
+ shop,
1116
+ apiToken,
1133
1117
  limit = 20,
1134
1118
  speedSeconds = 30,
1135
1119
  starColor,
1136
1120
  className
1137
1121
  }) {
1138
- const { data, loading } = useWidgetGlobals({ proxyUrl, limit });
1122
+ const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
1139
1123
  const trackRef = useRef(null);
1140
1124
  const reviews = data?.reviews ?? [];
1141
1125
  if (loading || reviews.length === 0) return null;
@@ -1196,7 +1180,9 @@ function ReviewTicker({
1196
1180
  import { useState as useState11 } from "react";
1197
1181
  import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1198
1182
  function FloatingReviewsTab({
1199
- proxyUrl,
1183
+ apiUrl,
1184
+ shop,
1185
+ apiToken,
1200
1186
  label = "Reviews",
1201
1187
  position = "right",
1202
1188
  color = "#111827",
@@ -1204,7 +1190,7 @@ function FloatingReviewsTab({
1204
1190
  starColor,
1205
1191
  className
1206
1192
  }) {
1207
- const { data } = useWidgetGlobals({ proxyUrl, limit });
1193
+ const { data } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
1208
1194
  const [open, setOpen] = useState11(false);
1209
1195
  const reviews = data?.reviews ?? [];
1210
1196
  const stats = data?.stats;
@@ -1281,8 +1267,8 @@ function FloatingReviewsTab({
1281
1267
 
1282
1268
  // src/components/TrustBadge.tsx
1283
1269
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1284
- function TrustBadge({ proxyUrl, style: badgeStyle = "pill", starColor, className }) {
1285
- const { data, loading } = useWidgetGlobals({ proxyUrl });
1270
+ function TrustBadge({ apiUrl, shop, apiToken, style: badgeStyle = "pill", starColor, className }) {
1271
+ const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken });
1286
1272
  if (loading || !data?.stats?.averageRating) return null;
1287
1273
  const avg = parseFloat(data.stats.averageRating);
1288
1274
  const count = data.stats.totalReviews;
@@ -1359,43 +1345,41 @@ import { useState as useState13 } from "react";
1359
1345
 
1360
1346
  // src/hooks/useForm.ts
1361
1347
  import { useEffect as useEffect5, useState as useState12 } from "react";
1362
- function useForm(proxyUrl, productId) {
1348
+ function useForm(creds, productId) {
1363
1349
  const [state, setState] = useState12({ form: null, loading: true, error: null });
1364
1350
  useEffect5(() => {
1365
1351
  let cancelled = false;
1366
- const params = new URLSearchParams({ productId, limit: "1", page: "1" });
1367
- fetch(`${proxyUrl}/reviews?${params.toString()}`).then((res) => {
1352
+ apiFetch(creds, "/api/reviews", { productId, limit: "1", page: "1" }).then((res) => {
1368
1353
  if (!res.ok) throw new Error(`Revova: form fetch failed (${res.status})`);
1369
1354
  return res.json();
1370
1355
  }).then(({ form }) => {
1371
1356
  if (!cancelled) setState({ form, loading: false, error: null });
1372
1357
  }).catch((err) => {
1373
1358
  if (!cancelled)
1374
- setState({
1375
- form: null,
1376
- loading: false,
1377
- error: err instanceof Error ? err : new Error(String(err))
1378
- });
1359
+ setState({ form: null, loading: false, error: err instanceof Error ? err : new Error(String(err)) });
1379
1360
  });
1380
1361
  return () => {
1381
1362
  cancelled = true;
1382
1363
  };
1383
- }, [proxyUrl, productId]);
1364
+ }, [creds.apiUrl, creds.shop, creds.apiToken, productId]);
1384
1365
  return state;
1385
1366
  }
1386
1367
 
1387
1368
  // src/components/FloatingReviewButton.tsx
1388
1369
  import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1389
1370
  function FloatingReviewButton({
1390
- proxyUrl,
1371
+ apiUrl,
1372
+ shop,
1373
+ apiToken,
1391
1374
  productId,
1392
1375
  text = "Write a Review",
1393
1376
  color = "#111827",
1394
1377
  position = "bottom-right",
1395
1378
  className
1396
1379
  }) {
1380
+ const creds = { apiUrl, shop, apiToken };
1397
1381
  const [open, setOpen] = useState13(false);
1398
- const { form, loading } = useForm(proxyUrl, productId);
1382
+ const { form, loading } = useForm(creds, productId);
1399
1383
  const posStyle = position === "bottom-right" ? { bottom: 24, right: 24 } : { bottom: 24, left: 24 };
1400
1384
  if (!loading && !form) return null;
1401
1385
  return /* @__PURE__ */ jsxs12(Fragment5, { children: [
@@ -1473,7 +1457,7 @@ function FloatingReviewButton({
1473
1457
  /* @__PURE__ */ jsx12(
1474
1458
  ReviewForm,
1475
1459
  {
1476
- proxyUrl,
1460
+ ...creds,
1477
1461
  productId,
1478
1462
  form,
1479
1463
  onSuccess: () => setOpen(false)
@@ -1489,7 +1473,7 @@ function FloatingReviewButton({
1489
1473
 
1490
1474
  // src/hooks/useHelpfulVote.ts
1491
1475
  import { useState as useState14, useCallback as useCallback4 } from "react";
1492
- function useHelpfulVote(proxyUrl) {
1476
+ function useHelpfulVote(creds) {
1493
1477
  const [loading, setLoading] = useState14(false);
1494
1478
  const [voted, setVoted] = useState14(false);
1495
1479
  const vote = useCallback4(
@@ -1497,9 +1481,8 @@ function useHelpfulVote(proxyUrl) {
1497
1481
  if (voted || loading) return;
1498
1482
  setLoading(true);
1499
1483
  try {
1500
- await fetch(`${proxyUrl}/helpful`, {
1484
+ await apiFetch(creds, "/api/helpful", {}, {
1501
1485
  method: "POST",
1502
- headers: { "Content-Type": "application/json" },
1503
1486
  body: JSON.stringify(payload)
1504
1487
  });
1505
1488
  setVoted(true);
@@ -1507,7 +1490,7 @@ function useHelpfulVote(proxyUrl) {
1507
1490
  setLoading(false);
1508
1491
  }
1509
1492
  },
1510
- [proxyUrl, voted, loading]
1493
+ [creds, voted, loading]
1511
1494
  );
1512
1495
  return { vote, loading, voted };
1513
1496
  }
@@ -1524,6 +1507,7 @@ export {
1524
1507
  SocialProofPopup,
1525
1508
  StarRating,
1526
1509
  TrustBadge,
1510
+ apiFetch,
1527
1511
  useForm,
1528
1512
  useHelpfulVote,
1529
1513
  useQnA,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revova/hydrogen",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Official Revova review widgets for Shopify Hydrogen storefronts",
5
5
  "author": "Revova",
6
6
  "license": "MIT",