@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.
- package/dist/_virtual/index4.js +2 -2
- package/dist/_virtual/index5.js +2 -3
- package/dist/_virtual/index5.js.map +1 -1
- package/dist/_virtual/index6.js +2 -2
- package/dist/_virtual/index7.js +3 -2
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/components/ErrorBoundary.js +19 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/MinisContainer.js +27 -16
- package/dist/components/MinisContainer.js.map +1 -1
- package/dist/components/atoms/image.js +35 -33
- package/dist/components/atoms/image.js.map +1 -1
- package/dist/mocks.js +32 -31
- package/dist/mocks.js.map +1 -1
- package/dist/providers/ImagePickerProvider.js +54 -55
- package/dist/providers/ImagePickerProvider.js.map +1 -1
- 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
- package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/generated-hook-maps/hook-scopes-map.json +16 -16
- package/package.json +2 -2
- package/src/components/ErrorBoundary.tsx +25 -0
- package/src/components/MinisContainer.tsx +24 -2
- package/src/components/atoms/image.test.tsx +41 -1
- package/src/components/atoms/image.tsx +8 -3
- package/src/hooks/storage/useAsyncStorage.test.ts +3 -2
- package/src/internal/useReportImpression.ts +33 -0
- package/src/internal/useReportInteraction.ts +33 -0
- package/src/mocks.ts +3 -2
- package/src/providers/ImagePickerProvider.test.tsx +82 -155
- package/src/providers/ImagePickerProvider.tsx +21 -33
- package/src/utils/getWindowLocationPathname.ts +6 -0
|
@@ -1,109 +1,108 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import { useRef as
|
|
3
|
-
import { useRequestPermissions as
|
|
4
|
-
const
|
|
5
|
-
function
|
|
6
|
-
const f =
|
|
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
|
-
|
|
14
|
-
|
|
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:
|
|
18
|
-
|
|
16
|
+
const { input: u, handler: c } = a.current;
|
|
17
|
+
u.removeEventListener("cancel", c), a.current = null;
|
|
19
18
|
}
|
|
20
|
-
}, []),
|
|
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
|
-
}, []),
|
|
25
|
-
(
|
|
26
|
-
const c =
|
|
27
|
-
c && n.current && (n.current(c), n.current = null, e.current = null,
|
|
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
|
-
[
|
|
30
|
-
),
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
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
|
|
38
|
-
e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null),
|
|
36
|
+
const l = () => {
|
|
37
|
+
e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null), r();
|
|
39
38
|
};
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
t.addEventListener("cancel", l), a.current = { input: t, handler: l }, p({ permission: "CAMERA" }).then(() => {
|
|
40
|
+
t.click();
|
|
42
41
|
}).catch(() => {
|
|
43
|
-
|
|
44
|
-
})
|
|
45
|
-
}), [
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
|
|
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
|
|
54
|
-
e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null),
|
|
52
|
+
const w = () => {
|
|
53
|
+
e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null), r();
|
|
55
54
|
};
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
})
|
|
58
|
+
t(new Error("Camera permission not granted")), n.current = null, e.current = null;
|
|
59
|
+
});
|
|
61
60
|
}),
|
|
62
|
-
[
|
|
61
|
+
[o, r, p]
|
|
63
62
|
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, [
|
|
67
|
-
const
|
|
63
|
+
R(() => () => {
|
|
64
|
+
o(), r();
|
|
65
|
+
}, [o, r]);
|
|
66
|
+
const v = I(
|
|
68
67
|
() => ({
|
|
69
68
|
openCamera: P,
|
|
70
|
-
openGallery:
|
|
69
|
+
openGallery: E
|
|
71
70
|
}),
|
|
72
|
-
[P,
|
|
71
|
+
[P, E]
|
|
73
72
|
);
|
|
74
|
-
return /* @__PURE__ */
|
|
73
|
+
return /* @__PURE__ */ x(k.Provider, { value: v, children: [
|
|
75
74
|
f,
|
|
76
|
-
/* @__PURE__ */
|
|
75
|
+
/* @__PURE__ */ m(
|
|
77
76
|
"input",
|
|
78
77
|
{
|
|
79
78
|
ref: C,
|
|
80
79
|
type: "file",
|
|
81
80
|
accept: "image/*",
|
|
82
|
-
onChange:
|
|
81
|
+
onChange: d,
|
|
83
82
|
style: { display: "none" },
|
|
84
83
|
"aria-hidden": "true"
|
|
85
84
|
}
|
|
86
85
|
),
|
|
87
|
-
/* @__PURE__ */
|
|
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:
|
|
93
|
+
onChange: d,
|
|
95
94
|
style: { display: "none" },
|
|
96
95
|
"aria-hidden": "true"
|
|
97
96
|
}
|
|
98
97
|
),
|
|
99
|
-
/* @__PURE__ */
|
|
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:
|
|
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
|
-
|
|
115
|
-
|
|
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":";;;AAkBA,MAAMA,IAAqBC,EAA8C,IAAI;AAEtE,SAASC,IAAwB;AAChC,QAAAC,IAAUC,EAAWJ,CAAkB;AAC7C,MAAI,CAACG;AACH,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAEK,SAAAA;AACT;AAMA,MAAME,IAAY,MAAM,QAAQ,aAAa,aAAa;AAE1C,SAAAC,EAAoB,EAAC,UAAAC,KAAqC;AAClE,QAAAC,IAAkBC,EAAyB,IAAI,GAC/CC,IAAsBD,EAAyB,IAAI,GACnDE,IAAqBF,EAAyB,IAAI,GAClDG,IAAaH,EAAsC,IAAI,GACvDI,IAAYJ,EAAyC,IAAI,GACzDK,IAAyBL,EAGrB,IAAI,GAER,EAAC,mBAAAM,EAAiB,IAAIC,EAAsB,GAE5CC,IAAuBC,EAAY,MAAM;AAC7C,QAAIJ,EAAuB,SAAS;AAClC,YAAM,EAAC,OAAAK,GAAO,SAAAC,EAAO,IAAIN,EAAuB;AAC1C,MAAAK,EAAA,oBAAoB,UAAUC,CAAO,GAC3CN,EAAuB,UAAU;AAAA,IAAA;AAAA,EAErC,GAAG,EAAE,GAECO,IAAuBH,EAAY,MAAM;AAC7C,IAAIL,EAAU,YACFA,EAAA;AAAA,MACR,IAAI,MAAM,kDAAkD;AAAA,IAC9D,GACAD,EAAW,UAAU,MACrBC,EAAU,UAAU;AAAA,EAExB,GAAG,EAAE,GAECS,IAAmBJ;AAAA,IACvB,CAACK,MAA+C;AAC9C,YAAMC,IAAOD,EAAM,OAAO,QAAQ,CAAC;AAE/B,MAAAC,KAAQZ,EAAW,YACrBA,EAAW,QAAQY,CAAI,GAEvBZ,EAAW,UAAU,MACrBC,EAAU,UAAU,MAECI,EAAA,IAGvBM,EAAM,OAAO,QAAQ;AAAA,IACvB;AAAA,IACA,CAACN,CAAoB;AAAA,EACvB,GAEMQ,IAAcP,EAAY,MACvB,IAAI,QAAc,CAACQ,GAASC,MAAW;AACvB,IAAAN,EAAA,GACAJ,EAAA,GAErBL,EAAW,UAAUc,GACrBb,EAAU,UAAUc;AAEpB,UAAMR,IAAQX,EAAgB;AAE9B,QAAI,CAACW,GAAO;AACH,MAAAQ,EAAA,IAAI,MAAM,yBAAyB,CAAC,GAC3Cf,EAAW,UAAU,MACrBC,EAAU,UAAU;AACpB;AAAA,IAAA;AAGF,UAAMe,IAAe,MAAM;AACzB,MAAIf,EAAU,YACZA,EAAU,QAAQ,IAAI,MAAM,+BAA+B,CAAC,GAC5DD,EAAW,UAAU,MACrBC,EAAU,UAAU,OAEDI,EAAA;AAAA,IACvB;AAEM,IAAAE,EAAA,iBAAiB,UAAUS,CAAY,GAC7Cd,EAAuB,UAAU,EAAC,OAAAK,GAAO,SAASS,EAAY,GAE1DvB,MAEFU,EAAkB,EAAC,YAAY,SAAA,CAAS,EACrC,KAAK,MAAM;AAEV,MAAAI,EAAM,MAAM;AAAA,IAAA,CACb,EACA,MAAM,MAAM;AAEX,MAAAA,EAAM,MAAM;AAAA,IAAA,CACb,IAEHA,EAAM,MAAM;AAAA,EACd,CACD,GACA,CAACE,GAAsBJ,GAAsBF,CAAiB,CAAC,GAE5Dc,IAAaX;AAAA,IACjB,CAACY,IAA6B,WACrB,IAAI,QAAc,CAACJ,GAASC,MAAW;AACvB,MAAAN,EAAA,GACAJ,EAAA,GAErBL,EAAW,UAAUc,GACrBb,EAAU,UAAUc;AAEpB,YAAMR,IACJW,MAAiB,UACbpB,EAAoB,UACpBC,EAAmB;AAEzB,UAAI,CAACQ,GAAO;AACH,QAAAQ,EAAA,IAAI,MAAM,wBAAwB,CAAC,GAC1Cf,EAAW,UAAU,MACrBC,EAAU,UAAU;AACpB;AAAA,MAAA;AAGF,YAAMe,IAAe,MAAM;AACzB,QAAIf,EAAU,YACZA,EAAU,QAAQ,IAAI,MAAM,uBAAuB,CAAC,GACpDD,EAAW,UAAU,MACrBC,EAAU,UAAU,OAEDI,EAAA;AAAA,MACvB;AAEM,MAAAE,EAAA,iBAAiB,UAAUS,CAAY,GAC7Cd,EAAuB,UAAU,EAAC,OAAAK,GAAO,SAASS,EAAY,GAE1DvB,MAEgBU,EAAA,EAAC,YAAY,SAAQ,CAAC,EACrC,KAAK,CAAC,EAAC,SAAAgB,QAAa;AACnB,QAAIA,IACFZ,EAAM,MAAM,KAELQ,EAAA,IAAI,MAAM,+BAA+B,CAAC,GACjDf,EAAW,UAAU,MACrBC,EAAU,UAAU;AAAA,MACtB,CACD,EACA,MAAM,MAAM;AACJ,QAAAc,EAAA,IAAI,MAAM,+BAA+B,CAAC,GACjDf,EAAW,UAAU,MACrBC,EAAU,UAAU;AAAA,MAAA,CACrB,IAEHM,EAAM,MAAM;AAAA,IACd,CACD;AAAA,IAEH,CAACE,GAAsBJ,GAAsBF,CAAiB;AAAA,EAChE;AAEA,EAAAiB,EAAU,MACD,MAAM;AACU,IAAAX,EAAA,GACAJ,EAAA;AAAA,EACvB,GACC,CAACI,GAAsBJ,CAAoB,CAAC;AAE/C,QAAMgB,IAAwCC;AAAA,IAC5C,OAAO;AAAA,MACL,YAAAL;AAAA,MACA,aAAAJ;AAAA,IAAA;AAAA,IAEF,CAACI,GAAYJ,CAAW;AAAA,EAC1B;AAEA,SACG,gBAAAU,EAAAnC,EAAmB,UAAnB,EAA4B,OAAOiC,GACjC,UAAA;AAAA,IAAA1B;AAAA,IACD,gBAAA6B;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK5B;AAAA,QACL,MAAK;AAAA,QACL,QAAO;AAAA,QACP,UAAUc;AAAA,QACV,OAAO,EAAC,SAAS,OAAM;AAAA,QACvB,eAAY;AAAA,MAAA;AAAA,IACd;AAAA,IACA,gBAAAc;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK1B;AAAA,QACL,MAAK;AAAA,QACL,QAAO;AAAA,QACP,SAAQ;AAAA,QACR,UAAUY;AAAA,QACV,OAAO,EAAC,SAAS,OAAM;AAAA,QACvB,eAAY;AAAA,MAAA;AAAA,IACd;AAAA,IACA,gBAAAc;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAKzB;AAAA,QACL,MAAK;AAAA,QACL,QAAO;AAAA,QACP,SAAQ;AAAA,QACR,UAAUW;AAAA,QACV,OAAO,EAAC,SAAS,OAAM;AAAA,QACvB,eAAY;AAAA,MAAA;AAAA,IAAA;AAAA,EACd,GACF;AAEJ;"}
|
|
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 { __module as q } from "../../../../../../../../_virtual/
|
|
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/
|
|
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,48 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"useBuyerAttributes": [
|
|
3
|
-
"
|
|
3
|
+
"profile"
|
|
4
4
|
],
|
|
5
5
|
"useCurrentUser": [
|
|
6
|
-
"
|
|
6
|
+
"user_settings:read"
|
|
7
7
|
],
|
|
8
8
|
"useFollowedShops": [
|
|
9
|
-
"
|
|
9
|
+
"shops:follows:read"
|
|
10
10
|
],
|
|
11
11
|
"useFollowedShopsActions": [
|
|
12
|
-
"
|
|
12
|
+
"shops:follows:write"
|
|
13
13
|
],
|
|
14
14
|
"useGenerateUserToken": [
|
|
15
|
-
"
|
|
15
|
+
"openid"
|
|
16
16
|
],
|
|
17
17
|
"useOrders": [
|
|
18
|
-
"
|
|
18
|
+
"orders"
|
|
19
19
|
],
|
|
20
20
|
"useProductList": [
|
|
21
|
-
"
|
|
21
|
+
"product_list:read"
|
|
22
22
|
],
|
|
23
23
|
"useProductListActions": [
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"product_list:write",
|
|
25
|
+
"product_list_item:write"
|
|
26
26
|
],
|
|
27
27
|
"useProductLists": [
|
|
28
|
-
"
|
|
28
|
+
"product_list:read"
|
|
29
29
|
],
|
|
30
30
|
"useRecentProducts": [
|
|
31
|
-
"
|
|
31
|
+
"products:recent:read"
|
|
32
32
|
],
|
|
33
33
|
"useRecentShops": [
|
|
34
|
-
"
|
|
34
|
+
"shops:recent:read"
|
|
35
35
|
],
|
|
36
36
|
"useRecommendedProducts": [
|
|
37
|
-
"
|
|
37
|
+
"products:recommendations:read"
|
|
38
38
|
],
|
|
39
39
|
"useRecommendedShops": [
|
|
40
|
-
"
|
|
40
|
+
"shops:recommendations:read"
|
|
41
41
|
],
|
|
42
42
|
"useSavedProducts": [
|
|
43
|
-
"
|
|
43
|
+
"product_list:read"
|
|
44
44
|
],
|
|
45
45
|
"useSavedProductsActions": [
|
|
46
|
-
"
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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('
|
|
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:
|
|
252
|
+
getPersistedItem: null,
|
|
253
253
|
setPersistedItem: undefined,
|
|
254
254
|
removePersistedItem: undefined,
|
|
255
255
|
getAllPersistedKeys: ['key1', 'key2', 'key3'],
|
|
256
256
|
clearPersistedItems: undefined,
|
|
257
|
-
getInternalPersistedItem:
|
|
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> = {}
|