@shopify/shop-minis-react 0.1.8 → 0.2.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.
Files changed (33) hide show
  1. package/dist/_virtual/index4.js +2 -2
  2. package/dist/_virtual/index5.js +2 -3
  3. package/dist/_virtual/index5.js.map +1 -1
  4. package/dist/_virtual/index6.js +2 -2
  5. package/dist/_virtual/index7.js +3 -2
  6. package/dist/_virtual/index7.js.map +1 -1
  7. package/dist/components/ErrorBoundary.js +19 -0
  8. package/dist/components/ErrorBoundary.js.map +1 -0
  9. package/dist/components/MinisContainer.js +27 -16
  10. package/dist/components/MinisContainer.js.map +1 -1
  11. package/dist/components/atoms/image.js +35 -33
  12. package/dist/components/atoms/image.js.map +1 -1
  13. package/dist/mocks.js +32 -31
  14. package/dist/mocks.js.map +1 -1
  15. package/dist/providers/ImagePickerProvider.js +54 -55
  16. package/dist/providers/ImagePickerProvider.js.map +1 -1
  17. package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
  18. package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
  19. package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
  20. package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
  21. package/generated-hook-maps/hook-scopes-map.json +16 -16
  22. package/package.json +2 -2
  23. package/src/components/ErrorBoundary.tsx +25 -0
  24. package/src/components/MinisContainer.tsx +24 -2
  25. package/src/components/atoms/image.test.tsx +41 -1
  26. package/src/components/atoms/image.tsx +8 -3
  27. package/src/hooks/storage/useAsyncStorage.test.ts +3 -2
  28. package/src/internal/useReportImpression.ts +33 -0
  29. package/src/internal/useReportInteraction.ts +33 -0
  30. package/src/mocks.ts +3 -2
  31. package/src/providers/ImagePickerProvider.test.tsx +82 -155
  32. package/src/providers/ImagePickerProvider.tsx +21 -33
  33. package/src/utils/getWindowLocationPathname.ts +6 -0
@@ -1,109 +1,108 @@
1
- import { jsxs as R, jsx as p } from "react/jsx-runtime";
2
- import { useRef as o, useCallback as s, useEffect as I, useMemo as b, createContext as A, useContext as L } from "react";
3
- import { useRequestPermissions as M } from "../hooks/util/useRequestPermissions.js";
4
- const v = A(null);
5
- function U() {
6
- const f = L(v);
1
+ import { jsxs as x, jsx as m } from "react/jsx-runtime";
2
+ import { useRef as i, useCallback as s, useEffect as R, useMemo as I, createContext as b, useContext as A } from "react";
3
+ import { useRequestPermissions as L } from "../hooks/util/useRequestPermissions.js";
4
+ const k = b(null);
5
+ function H() {
6
+ const f = A(k);
7
7
  if (!f)
8
8
  throw new Error(
9
9
  "useImagePickerContext must be used within an ImagePickerProvider"
10
10
  );
11
11
  return f;
12
12
  }
13
- const E = () => window?.minisParams?.platform === "android";
14
- function F({ children: f }) {
15
- const C = o(null), h = o(null), g = o(null), n = o(null), e = o(null), a = o(null), { requestPermission: d } = M(), t = s(() => {
13
+ function U({ children: f }) {
14
+ const C = i(null), h = i(null), g = i(null), n = i(null), e = i(null), a = i(null), { requestPermission: p } = L(), r = s(() => {
16
15
  if (a.current) {
17
- const { input: l, handler: c } = a.current;
18
- l.removeEventListener("cancel", c), a.current = null;
16
+ const { input: u, handler: c } = a.current;
17
+ u.removeEventListener("cancel", c), a.current = null;
19
18
  }
20
- }, []), i = s(() => {
19
+ }, []), o = s(() => {
21
20
  e.current && (e.current(
22
21
  new Error("New file picker opened before previous completed")
23
22
  ), n.current = null, e.current = null);
24
- }, []), m = s(
25
- (l) => {
26
- const c = l.target.files?.[0];
27
- c && n.current && (n.current(c), n.current = null, e.current = null, t()), l.target.value = "";
23
+ }, []), d = s(
24
+ (u) => {
25
+ const c = u.target.files?.[0];
26
+ c && n.current && (n.current(c), n.current = null, e.current = null, r()), u.target.value = "";
28
27
  },
29
- [t]
30
- ), w = s(() => new Promise((l, c) => {
31
- i(), t(), n.current = l, e.current = c;
32
- const r = C.current;
33
- if (!r) {
28
+ [r]
29
+ ), E = s(() => new Promise((u, c) => {
30
+ o(), r(), n.current = u, e.current = c;
31
+ const t = C.current;
32
+ if (!t) {
34
33
  c(new Error("Gallery input not found")), n.current = null, e.current = null;
35
34
  return;
36
35
  }
37
- const u = () => {
38
- e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null), t();
36
+ const l = () => {
37
+ e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null), r();
39
38
  };
40
- r.addEventListener("cancel", u), a.current = { input: r, handler: u }, E() ? d({ permission: "CAMERA" }).then(() => {
41
- r.click();
39
+ t.addEventListener("cancel", l), a.current = { input: t, handler: l }, p({ permission: "CAMERA" }).then(() => {
40
+ t.click();
42
41
  }).catch(() => {
43
- r.click();
44
- }) : r.click();
45
- }), [i, t, d]), P = s(
46
- (l = "back") => new Promise((c, r) => {
47
- i(), t(), n.current = c, e.current = r;
48
- const u = l === "front" ? h.current : g.current;
49
- if (!u) {
50
- r(new Error("Camera input not found")), n.current = null, e.current = null;
42
+ t.click();
43
+ });
44
+ }), [o, r, p]), P = s(
45
+ (u = "back") => new Promise((c, t) => {
46
+ o(), r(), n.current = c, e.current = t;
47
+ const l = u === "front" ? h.current : g.current;
48
+ if (!l) {
49
+ t(new Error("Camera input not found")), n.current = null, e.current = null;
51
50
  return;
52
51
  }
53
- const k = () => {
54
- e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null), t();
52
+ const w = () => {
53
+ e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null), r();
55
54
  };
56
- u.addEventListener("cancel", k), a.current = { input: u, handler: k }, E() ? d({ permission: "CAMERA" }).then(({ granted: x }) => {
57
- x ? u.click() : (r(new Error("Camera permission not granted")), n.current = null, e.current = null);
55
+ l.addEventListener("cancel", w), a.current = { input: l, handler: w }, p({ permission: "CAMERA" }).then(({ granted: y }) => {
56
+ y ? l.click() : (t(new Error("Camera permission not granted")), n.current = null, e.current = null);
58
57
  }).catch(() => {
59
- r(new Error("Camera permission not granted")), n.current = null, e.current = null;
60
- }) : u.click();
58
+ t(new Error("Camera permission not granted")), n.current = null, e.current = null;
59
+ });
61
60
  }),
62
- [i, t, d]
61
+ [o, r, p]
63
62
  );
64
- I(() => () => {
65
- i(), t();
66
- }, [i, t]);
67
- const y = b(
63
+ R(() => () => {
64
+ o(), r();
65
+ }, [o, r]);
66
+ const v = I(
68
67
  () => ({
69
68
  openCamera: P,
70
- openGallery: w
69
+ openGallery: E
71
70
  }),
72
- [P, w]
71
+ [P, E]
73
72
  );
74
- return /* @__PURE__ */ R(v.Provider, { value: y, children: [
73
+ return /* @__PURE__ */ x(k.Provider, { value: v, children: [
75
74
  f,
76
- /* @__PURE__ */ p(
75
+ /* @__PURE__ */ m(
77
76
  "input",
78
77
  {
79
78
  ref: C,
80
79
  type: "file",
81
80
  accept: "image/*",
82
- onChange: m,
81
+ onChange: d,
83
82
  style: { display: "none" },
84
83
  "aria-hidden": "true"
85
84
  }
86
85
  ),
87
- /* @__PURE__ */ p(
86
+ /* @__PURE__ */ m(
88
87
  "input",
89
88
  {
90
89
  ref: h,
91
90
  type: "file",
92
91
  accept: "image/*",
93
92
  capture: "user",
94
- onChange: m,
93
+ onChange: d,
95
94
  style: { display: "none" },
96
95
  "aria-hidden": "true"
97
96
  }
98
97
  ),
99
- /* @__PURE__ */ p(
98
+ /* @__PURE__ */ m(
100
99
  "input",
101
100
  {
102
101
  ref: g,
103
102
  type: "file",
104
103
  accept: "image/*",
105
104
  capture: "environment",
106
- onChange: m,
105
+ onChange: d,
107
106
  style: { display: "none" },
108
107
  "aria-hidden": "true"
109
108
  }
@@ -111,7 +110,7 @@ function F({ children: f }) {
111
110
  ] });
112
111
  }
113
112
  export {
114
- F as ImagePickerProvider,
115
- U as useImagePickerContext
113
+ U as ImagePickerProvider,
114
+ H as useImagePickerContext
116
115
  };
117
116
  //# sourceMappingURL=ImagePickerProvider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImagePickerProvider.js","sources":["../../src/providers/ImagePickerProvider.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useRef,\n useCallback,\n useEffect,\n useMemo,\n} from 'react'\n\nimport {useRequestPermissions} from '../hooks/util/useRequestPermissions'\n\nexport type CameraFacing = 'front' | 'back'\n\ninterface ImagePickerContextValue {\n openCamera: (cameraFacing?: CameraFacing) => Promise<File>\n openGallery: () => Promise<File>\n}\n\nconst ImagePickerContext = createContext<ImagePickerContextValue | null>(null)\n\nexport function useImagePickerContext() {\n const context = useContext(ImagePickerContext)\n if (!context) {\n throw new Error(\n 'useImagePickerContext must be used within an ImagePickerProvider'\n )\n }\n return context\n}\n\ninterface ImagePickerProviderProps {\n children: React.ReactNode\n}\n\nconst isAndroid = () => window?.minisParams?.platform === 'android'\n\nexport function ImagePickerProvider({children}: ImagePickerProviderProps) {\n const galleryInputRef = useRef<HTMLInputElement>(null)\n const frontCameraInputRef = useRef<HTMLInputElement>(null)\n const backCameraInputRef = useRef<HTMLInputElement>(null)\n const resolveRef = useRef<((file: File) => void) | null>(null)\n const rejectRef = useRef<((reason: Error) => void) | null>(null)\n const activeCancelHandlerRef = useRef<{\n input: HTMLInputElement\n handler: () => void\n } | null>(null)\n\n const {requestPermission} = useRequestPermissions()\n\n const cleanupCancelHandler = useCallback(() => {\n if (activeCancelHandlerRef.current) {\n const {input, handler} = activeCancelHandlerRef.current\n input.removeEventListener('cancel', handler)\n activeCancelHandlerRef.current = null\n }\n }, [])\n\n const rejectPendingPromise = useCallback(() => {\n if (rejectRef.current) {\n rejectRef.current(\n new Error('New file picker opened before previous completed')\n )\n resolveRef.current = null\n rejectRef.current = null\n }\n }, [])\n\n const handleFileChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0]\n\n if (file && resolveRef.current) {\n resolveRef.current(file)\n\n resolveRef.current = null\n rejectRef.current = null\n\n cleanupCancelHandler()\n }\n\n event.target.value = ''\n },\n [cleanupCancelHandler]\n )\n\n const openGallery = useCallback(() => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input = galleryInputRef.current\n\n if (!input) {\n reject(new Error('Gallery input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled file selection'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n if (isAndroid()) {\n // Android requires explicit camera permission for camera picker\n requestPermission({permission: 'CAMERA'})\n .then(() => {\n // This will show both Camera and Gallery\n input.click()\n })\n .catch(() => {\n // Show only Gallery\n input.click()\n })\n } else {\n input.click()\n }\n })\n }, [rejectPendingPromise, cleanupCancelHandler, requestPermission])\n\n const openCamera = useCallback(\n (cameraFacing: CameraFacing = 'back') => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input =\n cameraFacing === 'front'\n ? frontCameraInputRef.current\n : backCameraInputRef.current\n\n if (!input) {\n reject(new Error('Camera input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled camera'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n if (isAndroid()) {\n // Android requires explicit camera permission\n requestPermission({permission: 'CAMERA'})\n .then(({granted}) => {\n if (granted) {\n input.click()\n } else {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n }\n })\n .catch(() => {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n })\n } else {\n input.click()\n }\n })\n },\n [rejectPendingPromise, cleanupCancelHandler, requestPermission]\n )\n\n useEffect(() => {\n return () => {\n rejectPendingPromise()\n cleanupCancelHandler()\n }\n }, [rejectPendingPromise, cleanupCancelHandler])\n\n const contextValue: ImagePickerContextValue = useMemo(\n () => ({\n openCamera,\n openGallery,\n }),\n [openCamera, openGallery]\n )\n\n return (\n <ImagePickerContext.Provider value={contextValue}>\n {children}\n <input\n ref={galleryInputRef}\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={frontCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"user\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={backCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"environment\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n </ImagePickerContext.Provider>\n )\n}\n"],"names":["ImagePickerContext","createContext","useImagePickerContext","context","useContext","isAndroid","ImagePickerProvider","children","galleryInputRef","useRef","frontCameraInputRef","backCameraInputRef","resolveRef","rejectRef","activeCancelHandlerRef","requestPermission","useRequestPermissions","cleanupCancelHandler","useCallback","input","handler","rejectPendingPromise","handleFileChange","event","file","openGallery","resolve","reject","handleCancel","openCamera","cameraFacing","granted","useEffect","contextValue","useMemo","jsxs","jsx"],"mappings":"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"}
1
+ {"version":3,"file":"ImagePickerProvider.js","sources":["../../src/providers/ImagePickerProvider.tsx"],"sourcesContent":["import React, {\n createContext,\n useContext,\n useRef,\n useCallback,\n useEffect,\n useMemo,\n} from 'react'\n\nimport {useRequestPermissions} from '../hooks/util/useRequestPermissions'\n\nexport type CameraFacing = 'front' | 'back'\n\ninterface ImagePickerContextValue {\n openCamera: (cameraFacing?: CameraFacing) => Promise<File>\n openGallery: () => Promise<File>\n}\n\nconst ImagePickerContext = createContext<ImagePickerContextValue | null>(null)\n\nexport function useImagePickerContext() {\n const context = useContext(ImagePickerContext)\n if (!context) {\n throw new Error(\n 'useImagePickerContext must be used within an ImagePickerProvider'\n )\n }\n return context\n}\n\ninterface ImagePickerProviderProps {\n children: React.ReactNode\n}\n\nexport function ImagePickerProvider({children}: ImagePickerProviderProps) {\n const galleryInputRef = useRef<HTMLInputElement>(null)\n const frontCameraInputRef = useRef<HTMLInputElement>(null)\n const backCameraInputRef = useRef<HTMLInputElement>(null)\n const resolveRef = useRef<((file: File) => void) | null>(null)\n const rejectRef = useRef<((reason: Error) => void) | null>(null)\n const activeCancelHandlerRef = useRef<{\n input: HTMLInputElement\n handler: () => void\n } | null>(null)\n\n const {requestPermission} = useRequestPermissions()\n\n const cleanupCancelHandler = useCallback(() => {\n if (activeCancelHandlerRef.current) {\n const {input, handler} = activeCancelHandlerRef.current\n input.removeEventListener('cancel', handler)\n activeCancelHandlerRef.current = null\n }\n }, [])\n\n const rejectPendingPromise = useCallback(() => {\n if (rejectRef.current) {\n rejectRef.current(\n new Error('New file picker opened before previous completed')\n )\n resolveRef.current = null\n rejectRef.current = null\n }\n }, [])\n\n const handleFileChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0]\n\n if (file && resolveRef.current) {\n resolveRef.current(file)\n\n resolveRef.current = null\n rejectRef.current = null\n\n cleanupCancelHandler()\n }\n\n event.target.value = ''\n },\n [cleanupCancelHandler]\n )\n\n const openGallery = useCallback(() => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input = galleryInputRef.current\n\n if (!input) {\n reject(new Error('Gallery input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled file selection'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n requestPermission({permission: 'CAMERA'})\n .then(() => {\n // This will show both Camera and Gallery\n input.click()\n })\n .catch(() => {\n // Show only Gallery\n input.click()\n })\n })\n }, [rejectPendingPromise, cleanupCancelHandler, requestPermission])\n\n const openCamera = useCallback(\n (cameraFacing: CameraFacing = 'back') => {\n return new Promise<File>((resolve, reject) => {\n rejectPendingPromise()\n cleanupCancelHandler()\n\n resolveRef.current = resolve\n rejectRef.current = reject\n\n const input =\n cameraFacing === 'front'\n ? frontCameraInputRef.current\n : backCameraInputRef.current\n\n if (!input) {\n reject(new Error('Camera input not found'))\n resolveRef.current = null\n rejectRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n rejectRef.current(new Error('User cancelled camera'))\n resolveRef.current = null\n rejectRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n requestPermission({permission: 'CAMERA'})\n .then(({granted}) => {\n if (granted) {\n input.click()\n } else {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n }\n })\n .catch(() => {\n reject(new Error('Camera permission not granted'))\n resolveRef.current = null\n rejectRef.current = null\n })\n })\n },\n [rejectPendingPromise, cleanupCancelHandler, requestPermission]\n )\n\n useEffect(() => {\n return () => {\n rejectPendingPromise()\n cleanupCancelHandler()\n }\n }, [rejectPendingPromise, cleanupCancelHandler])\n\n const contextValue: ImagePickerContextValue = useMemo(\n () => ({\n openCamera,\n openGallery,\n }),\n [openCamera, openGallery]\n )\n\n return (\n <ImagePickerContext.Provider value={contextValue}>\n {children}\n <input\n ref={galleryInputRef}\n type=\"file\"\n accept=\"image/*\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={frontCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"user\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n <input\n ref={backCameraInputRef}\n type=\"file\"\n accept=\"image/*\"\n capture=\"environment\"\n onChange={handleFileChange}\n style={{display: 'none'}}\n aria-hidden=\"true\"\n />\n </ImagePickerContext.Provider>\n )\n}\n"],"names":["ImagePickerContext","createContext","useImagePickerContext","context","useContext","ImagePickerProvider","children","galleryInputRef","useRef","frontCameraInputRef","backCameraInputRef","resolveRef","rejectRef","activeCancelHandlerRef","requestPermission","useRequestPermissions","cleanupCancelHandler","useCallback","input","handler","rejectPendingPromise","handleFileChange","event","file","openGallery","resolve","reject","handleCancel","openCamera","cameraFacing","granted","useEffect","contextValue","useMemo","jsxs","jsx"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { s as r } from "../../../../../../../../_virtual/index6.js";
1
+ import { s as r } from "../../../../../../../../_virtual/index7.js";
2
2
  function s() {
3
3
  return r.useSyncExternalStore(
4
4
  e,
@@ -1,4 +1,4 @@
1
- import { __module as q } from "../../../../../../../../_virtual/index4.js";
1
+ import { __module as q } from "../../../../../../../../_virtual/index5.js";
2
2
  import { __require as F } from "../../../../../global@4.4.0/node_modules/global/window.js";
3
3
  import { __require as N } from "../../../../../@babel_runtime@7.27.6/node_modules/@babel/runtime/helpers/extends.js";
4
4
  import { __require as J } from "../../../../../is-function@1.0.2/node_modules/is-function/index.js";
@@ -2,7 +2,7 @@ import L from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-ut
2
2
  import T from "../../../../../../../_virtual/window.js";
3
3
  import { forEachMediaGroup as Z } from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/media-groups.js";
4
4
  import J from "../../../../@videojs_vhs-utils@4.1.1/node_modules/@videojs/vhs-utils/es/decode-b64-to-uint8-array.js";
5
- import { l as Q } from "../../../../../../../_virtual/index5.js";
5
+ import { l as Q } from "../../../../../../../_virtual/index6.js";
6
6
  /*! @name mpd-parser @version 1.3.1 @license Apache-2.0 */
7
7
  const w = (e) => !!e && typeof e == "object", E = (...e) => e.reduce((n, t) => (typeof t != "object" || Object.keys(t).forEach((r) => {
8
8
  Array.isArray(n[r]) && Array.isArray(t[r]) ? n[r] = n[r].concat(t[r]) : w(n[r]) && w(t[r]) ? n[r] = E(n[r], t[r]) : n[r] = t[r];
@@ -1,4 +1,4 @@
1
- import { __exports as i } from "../../../../../../_virtual/index7.js";
1
+ import { __exports as i } from "../../../../../../_virtual/index4.js";
2
2
  var c;
3
3
  function d() {
4
4
  if (c) return i;
@@ -1,48 +1,48 @@
1
1
  {
2
2
  "useBuyerAttributes": [
3
- "PROFILE"
3
+ "profile"
4
4
  ],
5
5
  "useCurrentUser": [
6
- "USER_SETTINGS_READ"
6
+ "user_settings:read"
7
7
  ],
8
8
  "useFollowedShops": [
9
- "SHOPS_FOLLOWS_READ"
9
+ "shops:follows:read"
10
10
  ],
11
11
  "useFollowedShopsActions": [
12
- "SHOPS_FOLLOWS_WRITE"
12
+ "shops:follows:write"
13
13
  ],
14
14
  "useGenerateUserToken": [
15
- "OPENID"
15
+ "openid"
16
16
  ],
17
17
  "useOrders": [
18
- "ORDERS"
18
+ "orders"
19
19
  ],
20
20
  "useProductList": [
21
- "PRODUCT_LIST_READ"
21
+ "product_list:read"
22
22
  ],
23
23
  "useProductListActions": [
24
- "PRODUCT_LIST_ITEM_WRITE",
25
- "PRODUCT_LIST_WRITE"
24
+ "product_list:write",
25
+ "product_list_item:write"
26
26
  ],
27
27
  "useProductLists": [
28
- "PRODUCT_LIST_READ"
28
+ "product_list:read"
29
29
  ],
30
30
  "useRecentProducts": [
31
- "PRODUCTS_RECENT_READ"
31
+ "products:recent:read"
32
32
  ],
33
33
  "useRecentShops": [
34
- "SHOPS_RECENT_READ"
34
+ "shops:recent:read"
35
35
  ],
36
36
  "useRecommendedProducts": [
37
- "PRODUCTS_RECOMMENDATIONS_READ"
37
+ "products:recommendations:read"
38
38
  ],
39
39
  "useRecommendedShops": [
40
- "SHOPS_RECOMMENDATIONS_READ"
40
+ "shops:recommendations:read"
41
41
  ],
42
42
  "useSavedProducts": [
43
- "FAVORITES"
43
+ "product_list:read"
44
44
  ],
45
45
  "useSavedProductsActions": [
46
- "FAVORITES_WRITE"
46
+ "product_list:write"
47
47
  ]
48
48
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shopify/shop-minis-react",
3
3
  "license": "SEE LICENSE IN LICENSE.txt",
4
- "version": "0.1.8",
4
+ "version": "0.2.1",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
7
  "engines": {
@@ -43,7 +43,7 @@
43
43
  "typescript": ">=5.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@shopify/shop-minis-platform": "0.5.0",
46
+ "@shopify/shop-minis-platform": "0.7.0",
47
47
  "@tailwindcss/vite": "4.1.8",
48
48
  "@types/color": "3.0.6",
49
49
  "@types/lodash": "4.17.20",
@@ -0,0 +1,25 @@
1
+ import React, {Component} from 'react'
2
+
3
+ import {ReportErrorParams} from '@shopify/shop-minis-platform/actions'
4
+
5
+ interface ErrorBoundaryProps {
6
+ children: React.ReactNode
7
+ onError: (params: ReportErrorParams) => void
8
+ }
9
+
10
+ export class ErrorBoundary extends Component<ErrorBoundaryProps> {
11
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
12
+ // Report the error to the app
13
+ this.props.onError({
14
+ message: error.message,
15
+ stack: error.stack,
16
+ additionalContext: {
17
+ componentStack: errorInfo.componentStack,
18
+ },
19
+ })
20
+ }
21
+
22
+ render() {
23
+ return this.props.children
24
+ }
25
+ }
@@ -1,12 +1,30 @@
1
- import React, {useEffect, useState} from 'react'
1
+ import React, {useCallback, useEffect, useState} from 'react'
2
2
 
3
+ import {ReportErrorParams} from '@shopify/shop-minis-platform/actions'
4
+
5
+ import {useShopActions} from '../internal/useShopActions'
3
6
  import {injectMocks} from '../mocks'
4
7
  import {ImagePickerProvider} from '../providers/ImagePickerProvider'
5
8
 
9
+ import {ErrorBoundary} from './ErrorBoundary'
10
+
6
11
  injectMocks()
7
12
 
8
13
  export function MinisContainer({children}: {children: React.ReactNode}) {
9
14
  const [isSDKReady, setIsSDKReady] = useState(false)
15
+ const {reportError} = useShopActions()
16
+
17
+ const handleError = useCallback(
18
+ async (params: ReportErrorParams) => {
19
+ try {
20
+ await reportError(params)
21
+ } catch (error) {
22
+ // If reporting fails, at least log to console
23
+ console.error('Failed to report error to app:', error)
24
+ }
25
+ },
26
+ [reportError]
27
+ )
10
28
 
11
29
  useEffect(() => {
12
30
  // Function to check if SDK is ready
@@ -63,5 +81,9 @@ export function MinisContainer({children}: {children: React.ReactNode}) {
63
81
  )
64
82
  }
65
83
 
66
- return <ImagePickerProvider>{children}</ImagePickerProvider>
84
+ return (
85
+ <ErrorBoundary onError={handleError}>
86
+ <ImagePickerProvider>{children}</ImagePickerProvider>
87
+ </ErrorBoundary>
88
+ )
67
89
  }
@@ -76,12 +76,13 @@ describe('Image', () => {
76
76
  expect(wrapper.style.aspectRatio).toBe('16/9')
77
77
  })
78
78
 
79
- it('uses thumbhash as background when provided', () => {
79
+ it('uses thumbhash as background when provided with fixed aspect ratio', () => {
80
80
  const {container} = render(
81
81
  <Image
82
82
  src="https://example.com/image.jpg"
83
83
  alt="Test image"
84
84
  thumbhash="testThumbhash"
85
+ aspectRatio="1"
85
86
  />
86
87
  )
87
88
 
@@ -91,6 +92,45 @@ describe('Image', () => {
91
92
  )
92
93
  })
93
94
 
95
+ it('renders with natural sizing when aspectRatio is auto', () => {
96
+ const {container} = render(
97
+ <Image
98
+ src="https://example.com/image.jpg"
99
+ alt="Test image"
100
+ aspectRatio="auto"
101
+ className="custom-class"
102
+ />
103
+ )
104
+
105
+ const wrapper = container.firstChild as HTMLElement
106
+ const img = screen.getByRole('img')
107
+
108
+ expect(wrapper.tagName).toBe('DIV')
109
+ expect(wrapper).toHaveClass('custom-class')
110
+ expect(wrapper.style.aspectRatio).toBe('')
111
+ expect(img).toHaveClass('w-full', 'h-auto')
112
+ expect(img).toHaveAttribute('src', 'https://example.com/image.jpg')
113
+ })
114
+
115
+ it('preserves thumbhash with natural sizing', () => {
116
+ const {container} = render(
117
+ <Image
118
+ src="https://example.com/image.jpg"
119
+ alt="Test image"
120
+ aspectRatio="auto"
121
+ thumbhash="testThumbhash"
122
+ />
123
+ )
124
+
125
+ const wrapper = container.firstChild as HTMLElement
126
+ const img = screen.getByRole('img')
127
+
128
+ expect(wrapper.style.backgroundImage).toContain(
129
+ 'data:image/png;base64,testThumbhash'
130
+ )
131
+ expect(img).toHaveClass('w-full', 'h-auto')
132
+ })
133
+
94
134
  it('passes additional props to img element', () => {
95
135
  render(
96
136
  <Image
@@ -17,6 +17,7 @@ type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
17
17
  file?: File
18
18
  thumbhash?: string | null
19
19
  aspectRatio?: number | string
20
+ objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none'
20
21
  }
21
22
 
22
23
  export const Image = memo(function Image(props: ImageProps) {
@@ -28,6 +29,7 @@ export const Image = memo(function Image(props: ImageProps) {
28
29
  className,
29
30
  style,
30
31
  aspectRatio = 'auto',
32
+ objectFit = 'contain',
31
33
  ...restProps
32
34
  } = props
33
35
 
@@ -71,10 +73,10 @@ export const Image = memo(function Image(props: ImageProps) {
71
73
 
72
74
  return (
73
75
  <div
74
- className={cn('relative w-full ', className)}
76
+ className={cn('relative w-full', className)}
75
77
  style={{
76
78
  ...style,
77
- aspectRatio,
79
+ ...(aspectRatio !== 'auto' && {aspectRatio}),
78
80
  backgroundImage: thumbhashDataURL
79
81
  ? `url(${thumbhashDataURL})`
80
82
  : undefined,
@@ -84,7 +86,10 @@ export const Image = memo(function Image(props: ImageProps) {
84
86
  >
85
87
  <img
86
88
  className={cn(
87
- 'absolute inset-0 opacity-0 size-full object-cover',
89
+ aspectRatio === 'auto'
90
+ ? 'opacity-0 w-full h-auto'
91
+ : 'absolute inset-0 opacity-0 size-full',
92
+ `object-${objectFit}`,
88
93
  isLoaded && 'opacity-100'
89
94
  )}
90
95
  src={imageSrc}
@@ -29,7 +29,7 @@ describe('useAsyncStorage', () => {
29
29
 
30
30
  // Set up mock actions with proper implementations
31
31
  mockActions = {
32
- getPersistedItem: vi.fn().mockResolvedValue('stored-value'),
32
+ getPersistedItem: vi.fn().mockResolvedValue(null),
33
33
  setPersistedItem: vi.fn().mockResolvedValue(undefined),
34
34
  removePersistedItem: vi.fn().mockResolvedValue(undefined),
35
35
  getAllPersistedKeys: vi.fn().mockResolvedValue(['key1', 'key2', 'key3']),
@@ -65,11 +65,12 @@ describe('useAsyncStorage', () => {
65
65
 
66
66
  describe('getItem', () => {
67
67
  it('calls getPersistedItem with correct parameters', async () => {
68
+ mockActions.getPersistedItem.mockResolvedValue('test-value')
68
69
  const {result} = renderHook(() => useAsyncStorage())
69
70
 
70
71
  await act(async () => {
71
72
  const value = await result.current.getItem({key: 'test-key'})
72
- expect(value).toBe('stored-value')
73
+ expect(value).toBe('test-value')
73
74
  })
74
75
 
75
76
  expect(mockActions.getPersistedItem).toHaveBeenCalledWith({
@@ -0,0 +1,33 @@
1
+ import {useCallback} from 'react'
2
+
3
+ import {ReportImpressionParams} from '@shopify/shop-minis-platform/actions'
4
+
5
+ import {getWindowLocationPathname} from '../utils/getWindowLocationPathname'
6
+
7
+ import {useHandleAction} from './useHandleAction'
8
+ import {useShopActions} from './useShopActions'
9
+
10
+ interface UseReportImpressionReturns {
11
+ /**
12
+ * Report an impression event for analytics.
13
+ */
14
+ reportImpression: (params: ReportImpressionParams) => Promise<void>
15
+ }
16
+
17
+ export const useReportImpression = (): UseReportImpressionReturns => {
18
+ const {reportImpression} = useShopActions()
19
+ const handleAction = useHandleAction(reportImpression)
20
+
21
+ const report = useCallback(
22
+ (params: ReportImpressionParams) => {
23
+ const enrichedParams = {
24
+ ...params,
25
+ pageValue: params.pageValue || getWindowLocationPathname(),
26
+ }
27
+ return handleAction(enrichedParams)
28
+ },
29
+ [handleAction]
30
+ )
31
+
32
+ return {reportImpression: report}
33
+ }
@@ -0,0 +1,33 @@
1
+ import {useCallback} from 'react'
2
+
3
+ import {ReportInteractionParams} from '@shopify/shop-minis-platform/actions'
4
+
5
+ import {getWindowLocationPathname} from '../utils/getWindowLocationPathname'
6
+
7
+ import {useHandleAction} from './useHandleAction'
8
+ import {useShopActions} from './useShopActions'
9
+
10
+ interface UseReportInteractionReturns {
11
+ /**
12
+ * Report a user interaction event for analytics.
13
+ */
14
+ reportInteraction: (params: ReportInteractionParams) => Promise<void>
15
+ }
16
+
17
+ export const useReportInteraction = (): UseReportInteractionReturns => {
18
+ const {reportInteraction} = useShopActions()
19
+ const handleAction = useHandleAction(reportInteraction)
20
+
21
+ const report = useCallback(
22
+ (params: ReportInteractionParams) => {
23
+ const enrichedParams = {
24
+ ...params,
25
+ pageValue: params.pageValue || getWindowLocationPathname(),
26
+ }
27
+ return handleAction(enrichedParams)
28
+ },
29
+ [handleAction]
30
+ )
31
+
32
+ return {reportInteraction: report}
33
+ }
package/src/mocks.ts CHANGED
@@ -249,12 +249,12 @@ export function makeMockActions(): ShopActions {
249
249
  },
250
250
  ],
251
251
  },
252
- getPersistedItem: 'stored-value',
252
+ getPersistedItem: null,
253
253
  setPersistedItem: undefined,
254
254
  removePersistedItem: undefined,
255
255
  getAllPersistedKeys: ['key1', 'key2', 'key3'],
256
256
  clearPersistedItems: undefined,
257
- getInternalPersistedItem: 'internal-value',
257
+ getInternalPersistedItem: null,
258
258
  setInternalPersistedItem: undefined,
259
259
  removeInternalPersistedItem: undefined,
260
260
  getAllInternalPersistedKeys: ['internal-key1', 'internal-key2'],
@@ -461,6 +461,7 @@ export function makeMockActions(): ShopActions {
461
461
  requestPermission: {
462
462
  granted: true,
463
463
  },
464
+ reportError: undefined,
464
465
  } as const
465
466
 
466
467
  const mock: Partial<ShopActions> = {}