@shopify/shop-minis-react 0.2.1 → 0.2.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/internal/useReportInteraction.js +21 -0
- package/dist/internal/useReportInteraction.js.map +1 -0
- package/dist/providers/ImagePickerProvider.js +122 -63
- package/dist/providers/ImagePickerProvider.js.map +1 -1
- package/dist/utils/getWindowLocationPathname.js +7 -0
- package/dist/utils/getWindowLocationPathname.js.map +1 -0
- package/package.json +1 -2
- package/src/providers/ImagePickerProvider.test.tsx +391 -0
- package/src/providers/ImagePickerProvider.tsx +93 -12
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCallback as n } from "react";
|
|
2
|
+
import { getWindowLocationPathname as a } from "../utils/getWindowLocationPathname.js";
|
|
3
|
+
import { useHandleAction as c } from "./useHandleAction.js";
|
|
4
|
+
import { useShopActions as i } from "./useShopActions.js";
|
|
5
|
+
const d = () => {
|
|
6
|
+
const { reportInteraction: r } = i(), o = c(r);
|
|
7
|
+
return { reportInteraction: n(
|
|
8
|
+
(t) => {
|
|
9
|
+
const e = {
|
|
10
|
+
...t,
|
|
11
|
+
pageValue: t.pageValue || a()
|
|
12
|
+
};
|
|
13
|
+
return o(e);
|
|
14
|
+
},
|
|
15
|
+
[o]
|
|
16
|
+
) };
|
|
17
|
+
};
|
|
18
|
+
export {
|
|
19
|
+
d as useReportInteraction
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=useReportInteraction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useReportInteraction.js","sources":["../../src/internal/useReportInteraction.ts"],"sourcesContent":["import {useCallback} from 'react'\n\nimport {ReportInteractionParams} from '@shopify/shop-minis-platform/actions'\n\nimport {getWindowLocationPathname} from '../utils/getWindowLocationPathname'\n\nimport {useHandleAction} from './useHandleAction'\nimport {useShopActions} from './useShopActions'\n\ninterface UseReportInteractionReturns {\n /**\n * Report a user interaction event for analytics.\n */\n reportInteraction: (params: ReportInteractionParams) => Promise<void>\n}\n\nexport const useReportInteraction = (): UseReportInteractionReturns => {\n const {reportInteraction} = useShopActions()\n const handleAction = useHandleAction(reportInteraction)\n\n const report = useCallback(\n (params: ReportInteractionParams) => {\n const enrichedParams = {\n ...params,\n pageValue: params.pageValue || getWindowLocationPathname(),\n }\n return handleAction(enrichedParams)\n },\n [handleAction]\n )\n\n return {reportInteraction: report}\n}\n"],"names":["useReportInteraction","reportInteraction","useShopActions","handleAction","useHandleAction","useCallback","params","enrichedParams","getWindowLocationPathname"],"mappings":";;;;AAgBO,MAAMA,IAAuB,MAAmC;AAC/D,QAAA,EAAC,mBAAAC,EAAiB,IAAIC,EAAe,GACrCC,IAAeC,EAAgBH,CAAiB;AAa/C,SAAA,EAAC,mBAXOI;AAAA,IACb,CAACC,MAAoC;AACnC,YAAMC,IAAiB;AAAA,QACrB,GAAGD;AAAA,QACH,WAAWA,EAAO,aAAaE,EAA0B;AAAA,MAC3D;AACA,aAAOL,EAAaI,CAAc;AAAA,IACpC;AAAA,IACA,CAACJ,CAAY;AAAA,EACf,EAEiC;AACnC;"}
|
|
@@ -1,108 +1,167 @@
|
|
|
1
|
-
import { jsxs as x, jsx as
|
|
2
|
-
import { useRef as
|
|
1
|
+
import { jsxs as x, jsx as h } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as p, useCallback as d, useEffect as I, useMemo as V, createContext as b, useContext as A } from "react";
|
|
3
3
|
import { useRequestPermissions as L } from "../hooks/util/useRequestPermissions.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { useReportInteraction as M } from "../internal/useReportInteraction.js";
|
|
5
|
+
const R = b(null);
|
|
6
|
+
function F() {
|
|
7
|
+
const g = A(R);
|
|
8
|
+
if (!g)
|
|
8
9
|
throw new Error(
|
|
9
10
|
"useImagePickerContext must be used within an ImagePickerProvider"
|
|
10
11
|
);
|
|
11
|
-
return
|
|
12
|
+
return g;
|
|
12
13
|
}
|
|
13
|
-
function
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
const { input:
|
|
17
|
-
|
|
14
|
+
function N({ children: g }) {
|
|
15
|
+
const k = p(null), _ = p(null), E = p(null), t = p(null), e = p(null), f = p(null), n = p(null), { requestPermission: y } = L(), { reportInteraction: r } = M(), a = d(() => {
|
|
16
|
+
if (f.current) {
|
|
17
|
+
const { input: c, handler: i } = f.current;
|
|
18
|
+
c.removeEventListener("cancel", i), f.current = null;
|
|
18
19
|
}
|
|
19
|
-
}, []),
|
|
20
|
-
|
|
21
|
-
new Error(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
}, []), m = d(() => {
|
|
21
|
+
if (e.current) {
|
|
22
|
+
const c = new Error(
|
|
23
|
+
"New file picker opened before previous completed"
|
|
24
|
+
);
|
|
25
|
+
n.current === "gallery" ? r({
|
|
26
|
+
interactionType: "image_picker_error",
|
|
27
|
+
interactionValue: c.message
|
|
28
|
+
}) : n.current === "camera" && r({
|
|
29
|
+
interactionType: "camera_error",
|
|
30
|
+
interactionValue: c.message
|
|
31
|
+
}), e.current(c), t.current = null, e.current = null, n.current = null;
|
|
32
|
+
}
|
|
33
|
+
}, [r]), C = d(
|
|
34
|
+
(c) => {
|
|
35
|
+
const i = c.target.files?.[0];
|
|
36
|
+
i && t.current && (n.current === "gallery" ? r({
|
|
37
|
+
interactionType: "image_picker_success"
|
|
38
|
+
}) : n.current === "camera" && r({
|
|
39
|
+
interactionType: "camera_success"
|
|
40
|
+
}), t.current(i), t.current = null, e.current = null, n.current = null, a()), c.target.value = "";
|
|
27
41
|
},
|
|
28
|
-
[r]
|
|
29
|
-
),
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
33
|
-
|
|
42
|
+
[a, r]
|
|
43
|
+
), P = d(() => new Promise((c, i) => {
|
|
44
|
+
m(), a(), t.current = c, e.current = i, n.current = "gallery";
|
|
45
|
+
const u = k.current;
|
|
46
|
+
if (!u) {
|
|
47
|
+
const o = new Error("Gallery input not found");
|
|
48
|
+
r({
|
|
49
|
+
interactionType: "image_picker_error",
|
|
50
|
+
interactionValue: o.message
|
|
51
|
+
}), i(o), t.current = null, e.current = null, n.current = null;
|
|
34
52
|
return;
|
|
35
53
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
54
|
+
const s = () => {
|
|
55
|
+
if (e.current) {
|
|
56
|
+
const o = new Error("User cancelled file selection");
|
|
57
|
+
r({
|
|
58
|
+
interactionType: "image_picker_error",
|
|
59
|
+
interactionValue: o.message
|
|
60
|
+
}), e.current(o), t.current = null, e.current = null, n.current = null;
|
|
61
|
+
}
|
|
62
|
+
a();
|
|
38
63
|
};
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
u.addEventListener("cancel", s), f.current = { input: u, handler: s }, r({
|
|
65
|
+
interactionType: "image_picker_open"
|
|
66
|
+
}), y({ permission: "CAMERA" }).then(() => {
|
|
67
|
+
u.click();
|
|
41
68
|
}).catch(() => {
|
|
42
|
-
|
|
69
|
+
u.click();
|
|
43
70
|
});
|
|
44
|
-
}), [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
}), [
|
|
72
|
+
m,
|
|
73
|
+
a,
|
|
74
|
+
y,
|
|
75
|
+
r
|
|
76
|
+
]), v = d(
|
|
77
|
+
(c = "back") => new Promise((i, u) => {
|
|
78
|
+
m(), a(), t.current = i, e.current = u, n.current = "camera";
|
|
79
|
+
const s = c === "front" ? _.current : E.current;
|
|
80
|
+
if (!s) {
|
|
81
|
+
const l = new Error("Camera input not found");
|
|
82
|
+
r({
|
|
83
|
+
interactionType: "camera_error",
|
|
84
|
+
interactionValue: l.message
|
|
85
|
+
}), u(l), t.current = null, e.current = null, n.current = null;
|
|
50
86
|
return;
|
|
51
87
|
}
|
|
52
|
-
const
|
|
53
|
-
|
|
88
|
+
const o = () => {
|
|
89
|
+
if (e.current) {
|
|
90
|
+
const l = new Error("User cancelled camera");
|
|
91
|
+
r({
|
|
92
|
+
interactionType: "camera_error",
|
|
93
|
+
interactionValue: l.message
|
|
94
|
+
}), e.current(l), t.current = null, e.current = null, n.current = null;
|
|
95
|
+
}
|
|
96
|
+
a();
|
|
54
97
|
};
|
|
55
|
-
|
|
56
|
-
|
|
98
|
+
s.addEventListener("cancel", o), f.current = { input: s, handler: o }, r({
|
|
99
|
+
interactionType: "camera_open"
|
|
100
|
+
}), y({ permission: "CAMERA" }).then(({ granted: l }) => {
|
|
101
|
+
if (l)
|
|
102
|
+
s.click();
|
|
103
|
+
else {
|
|
104
|
+
const w = new Error("Camera permission not granted");
|
|
105
|
+
r({
|
|
106
|
+
interactionType: "camera_error",
|
|
107
|
+
interactionValue: w.message
|
|
108
|
+
}), u(w), t.current = null, e.current = null, n.current = null;
|
|
109
|
+
}
|
|
57
110
|
}).catch(() => {
|
|
58
|
-
|
|
111
|
+
const l = new Error("Camera permission not granted");
|
|
112
|
+
u(l), t.current = null, e.current = null, n.current = null;
|
|
59
113
|
});
|
|
60
114
|
}),
|
|
61
|
-
[
|
|
115
|
+
[
|
|
116
|
+
m,
|
|
117
|
+
a,
|
|
118
|
+
y,
|
|
119
|
+
r
|
|
120
|
+
]
|
|
62
121
|
);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}, [
|
|
66
|
-
const
|
|
122
|
+
I(() => () => {
|
|
123
|
+
m(), a();
|
|
124
|
+
}, [m, a]);
|
|
125
|
+
const T = V(
|
|
67
126
|
() => ({
|
|
68
|
-
openCamera:
|
|
69
|
-
openGallery:
|
|
127
|
+
openCamera: v,
|
|
128
|
+
openGallery: P
|
|
70
129
|
}),
|
|
71
|
-
[
|
|
130
|
+
[v, P]
|
|
72
131
|
);
|
|
73
|
-
return /* @__PURE__ */ x(
|
|
74
|
-
|
|
75
|
-
/* @__PURE__ */
|
|
132
|
+
return /* @__PURE__ */ x(R.Provider, { value: T, children: [
|
|
133
|
+
g,
|
|
134
|
+
/* @__PURE__ */ h(
|
|
76
135
|
"input",
|
|
77
136
|
{
|
|
78
|
-
ref:
|
|
137
|
+
ref: k,
|
|
79
138
|
type: "file",
|
|
80
139
|
accept: "image/*",
|
|
81
|
-
onChange:
|
|
140
|
+
onChange: C,
|
|
82
141
|
style: { display: "none" },
|
|
83
142
|
"aria-hidden": "true"
|
|
84
143
|
}
|
|
85
144
|
),
|
|
86
|
-
/* @__PURE__ */
|
|
145
|
+
/* @__PURE__ */ h(
|
|
87
146
|
"input",
|
|
88
147
|
{
|
|
89
|
-
ref:
|
|
148
|
+
ref: _,
|
|
90
149
|
type: "file",
|
|
91
150
|
accept: "image/*",
|
|
92
151
|
capture: "user",
|
|
93
|
-
onChange:
|
|
152
|
+
onChange: C,
|
|
94
153
|
style: { display: "none" },
|
|
95
154
|
"aria-hidden": "true"
|
|
96
155
|
}
|
|
97
156
|
),
|
|
98
|
-
/* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ h(
|
|
99
158
|
"input",
|
|
100
159
|
{
|
|
101
|
-
ref:
|
|
160
|
+
ref: E,
|
|
102
161
|
type: "file",
|
|
103
162
|
accept: "image/*",
|
|
104
163
|
capture: "environment",
|
|
105
|
-
onChange:
|
|
164
|
+
onChange: C,
|
|
106
165
|
style: { display: "none" },
|
|
107
166
|
"aria-hidden": "true"
|
|
108
167
|
}
|
|
@@ -110,7 +169,7 @@ function U({ children: f }) {
|
|
|
110
169
|
] });
|
|
111
170
|
}
|
|
112
171
|
export {
|
|
113
|
-
|
|
114
|
-
|
|
172
|
+
N as ImagePickerProvider,
|
|
173
|
+
F as useImagePickerContext
|
|
115
174
|
};
|
|
116
175
|
//# 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\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
|
+
{"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'\nimport {useReportInteraction} from '../internal/useReportInteraction'\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 const activeOperationRef = useRef<'gallery' | 'camera' | null>(null)\n\n const {requestPermission} = useRequestPermissions()\n const {reportInteraction} = useReportInteraction()\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 const error = new Error(\n 'New file picker opened before previous completed'\n )\n if (activeOperationRef.current === 'gallery') {\n reportInteraction({\n interactionType: 'image_picker_error',\n interactionValue: error.message,\n })\n } else if (activeOperationRef.current === 'camera') {\n reportInteraction({\n interactionType: 'camera_error',\n interactionValue: error.message,\n })\n }\n\n rejectRef.current(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n }\n }, [reportInteraction])\n\n const handleFileChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0]\n\n if (file && resolveRef.current) {\n // Report success based on the active operation\n if (activeOperationRef.current === 'gallery') {\n reportInteraction({\n interactionType: 'image_picker_success',\n })\n } else if (activeOperationRef.current === 'camera') {\n reportInteraction({\n interactionType: 'camera_success',\n })\n }\n\n resolveRef.current(file)\n\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n\n cleanupCancelHandler()\n }\n\n event.target.value = ''\n },\n [cleanupCancelHandler, reportInteraction]\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 activeOperationRef.current = 'gallery'\n\n const input = galleryInputRef.current\n\n if (!input) {\n const error = new Error('Gallery input not found')\n reportInteraction({\n interactionType: 'image_picker_error',\n interactionValue: error.message,\n })\n reject(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n const error = new Error('User cancelled file selection')\n reportInteraction({\n interactionType: 'image_picker_error',\n interactionValue: error.message,\n })\n rejectRef.current(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n reportInteraction({\n interactionType: 'image_picker_open',\n })\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 }, [\n rejectPendingPromise,\n cleanupCancelHandler,\n requestPermission,\n reportInteraction,\n ])\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 activeOperationRef.current = 'camera'\n\n const input =\n cameraFacing === 'front'\n ? frontCameraInputRef.current\n : backCameraInputRef.current\n\n if (!input) {\n const error = new Error('Camera input not found')\n reportInteraction({\n interactionType: 'camera_error',\n interactionValue: error.message,\n })\n reject(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n return\n }\n\n const handleCancel = () => {\n if (rejectRef.current) {\n const error = new Error('User cancelled camera')\n reportInteraction({\n interactionType: 'camera_error',\n interactionValue: error.message,\n })\n rejectRef.current(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n }\n cleanupCancelHandler()\n }\n\n input.addEventListener('cancel', handleCancel)\n activeCancelHandlerRef.current = {input, handler: handleCancel}\n\n reportInteraction({\n interactionType: 'camera_open',\n })\n\n requestPermission({permission: 'CAMERA'})\n .then(({granted}) => {\n if (granted) {\n input.click()\n } else {\n const error = new Error('Camera permission not granted')\n reportInteraction({\n interactionType: 'camera_error',\n interactionValue: error.message,\n })\n reject(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n }\n })\n .catch(() => {\n const error = new Error('Camera permission not granted')\n reject(error)\n resolveRef.current = null\n rejectRef.current = null\n activeOperationRef.current = null\n })\n })\n },\n [\n rejectPendingPromise,\n cleanupCancelHandler,\n requestPermission,\n reportInteraction,\n ]\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","activeOperationRef","requestPermission","useRequestPermissions","reportInteraction","useReportInteraction","cleanupCancelHandler","useCallback","input","handler","rejectPendingPromise","error","handleFileChange","event","file","openGallery","resolve","reject","handleCancel","openCamera","cameraFacing","granted","useEffect","contextValue","useMemo","jsxs","jsx"],"mappings":";;;;AAmBA,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;AAMgB,SAAAE,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,GACRM,IAAqBN,EAAoC,IAAI,GAE7D,EAAC,mBAAAO,EAAiB,IAAIC,EAAsB,GAC5C,EAAC,mBAAAC,EAAiB,IAAIC,EAAqB,GAE3CC,IAAuBC,EAAY,MAAM;AAC7C,QAAIP,EAAuB,SAAS;AAClC,YAAM,EAAC,OAAAQ,GAAO,SAAAC,EAAO,IAAIT,EAAuB;AAC1C,MAAAQ,EAAA,oBAAoB,UAAUC,CAAO,GAC3CT,EAAuB,UAAU;AAAA,IAAA;AAAA,EAErC,GAAG,EAAE,GAECU,IAAuBH,EAAY,MAAM;AAC7C,QAAIR,EAAU,SAAS;AACrB,YAAMY,IAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AACI,MAAAV,EAAmB,YAAY,YACfG,EAAA;AAAA,QAChB,iBAAiB;AAAA,QACjB,kBAAkBO,EAAM;AAAA,MAAA,CACzB,IACQV,EAAmB,YAAY,YACtBG,EAAA;AAAA,QAChB,iBAAiB;AAAA,QACjB,kBAAkBO,EAAM;AAAA,MAAA,CACzB,GAGHZ,EAAU,QAAQY,CAAK,GACvBb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAAA,IAAA;AAAA,EAC/B,GACC,CAACG,CAAiB,CAAC,GAEhBQ,IAAmBL;AAAA,IACvB,CAACM,MAA+C;AAC9C,YAAMC,IAAOD,EAAM,OAAO,QAAQ,CAAC;AAE/B,MAAAC,KAAQhB,EAAW,YAEjBG,EAAmB,YAAY,YACfG,EAAA;AAAA,QAChB,iBAAiB;AAAA,MAAA,CAClB,IACQH,EAAmB,YAAY,YACtBG,EAAA;AAAA,QAChB,iBAAiB;AAAA,MAAA,CAClB,GAGHN,EAAW,QAAQgB,CAAI,GAEvBhB,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU,MAERK,EAAA,IAGvBO,EAAM,OAAO,QAAQ;AAAA,IACvB;AAAA,IACA,CAACP,GAAsBF,CAAiB;AAAA,EAC1C,GAEMW,IAAcR,EAAY,MACvB,IAAI,QAAc,CAACS,GAASC,MAAW;AACvB,IAAAP,EAAA,GACAJ,EAAA,GAErBR,EAAW,UAAUkB,GACrBjB,EAAU,UAAUkB,GACpBhB,EAAmB,UAAU;AAE7B,UAAMO,IAAQd,EAAgB;AAE9B,QAAI,CAACc,GAAO;AACJ,YAAAG,IAAQ,IAAI,MAAM,yBAAyB;AAC/B,MAAAP,EAAA;AAAA,QAChB,iBAAiB;AAAA,QACjB,kBAAkBO,EAAM;AAAA,MAAA,CACzB,GACDM,EAAON,CAAK,GACZb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAC7B;AAAA,IAAA;AAGF,UAAMiB,IAAe,MAAM;AACzB,UAAInB,EAAU,SAAS;AACf,cAAAY,IAAQ,IAAI,MAAM,+BAA+B;AACrC,QAAAP,EAAA;AAAA,UAChB,iBAAiB;AAAA,UACjB,kBAAkBO,EAAM;AAAA,QAAA,CACzB,GACDZ,EAAU,QAAQY,CAAK,GACvBb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAAA,MAAA;AAEV,MAAAK,EAAA;AAAA,IACvB;AAEM,IAAAE,EAAA,iBAAiB,UAAUU,CAAY,GAC7ClB,EAAuB,UAAU,EAAC,OAAAQ,GAAO,SAASU,EAAY,GAE5Cd,EAAA;AAAA,MAChB,iBAAiB;AAAA,IAAA,CAClB,GAEDF,EAAkB,EAAC,YAAY,SAAA,CAAS,EACrC,KAAK,MAAM;AAEV,MAAAM,EAAM,MAAM;AAAA,IAAA,CACb,EACA,MAAM,MAAM;AAEX,MAAAA,EAAM,MAAM;AAAA,IAAA,CACb;AAAA,EAAA,CACJ,GACA;AAAA,IACDE;AAAA,IACAJ;AAAA,IACAJ;AAAA,IACAE;AAAA,EAAA,CACD,GAEKe,IAAaZ;AAAA,IACjB,CAACa,IAA6B,WACrB,IAAI,QAAc,CAACJ,GAASC,MAAW;AACvB,MAAAP,EAAA,GACAJ,EAAA,GAErBR,EAAW,UAAUkB,GACrBjB,EAAU,UAAUkB,GACpBhB,EAAmB,UAAU;AAE7B,YAAMO,IACJY,MAAiB,UACbxB,EAAoB,UACpBC,EAAmB;AAEzB,UAAI,CAACW,GAAO;AACJ,cAAAG,IAAQ,IAAI,MAAM,wBAAwB;AAC9B,QAAAP,EAAA;AAAA,UAChB,iBAAiB;AAAA,UACjB,kBAAkBO,EAAM;AAAA,QAAA,CACzB,GACDM,EAAON,CAAK,GACZb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAC7B;AAAA,MAAA;AAGF,YAAMiB,IAAe,MAAM;AACzB,YAAInB,EAAU,SAAS;AACf,gBAAAY,IAAQ,IAAI,MAAM,uBAAuB;AAC7B,UAAAP,EAAA;AAAA,YAChB,iBAAiB;AAAA,YACjB,kBAAkBO,EAAM;AAAA,UAAA,CACzB,GACDZ,EAAU,QAAQY,CAAK,GACvBb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAAA,QAAA;AAEV,QAAAK,EAAA;AAAA,MACvB;AAEM,MAAAE,EAAA,iBAAiB,UAAUU,CAAY,GAC7ClB,EAAuB,UAAU,EAAC,OAAAQ,GAAO,SAASU,EAAY,GAE5Cd,EAAA;AAAA,QAChB,iBAAiB;AAAA,MAAA,CAClB,GAEiBF,EAAA,EAAC,YAAY,SAAQ,CAAC,EACrC,KAAK,CAAC,EAAC,SAAAmB,QAAa;AACnB,YAAIA;AACF,UAAAb,EAAM,MAAM;AAAA,aACP;AACC,gBAAAG,IAAQ,IAAI,MAAM,+BAA+B;AACrC,UAAAP,EAAA;AAAA,YAChB,iBAAiB;AAAA,YACjB,kBAAkBO,EAAM;AAAA,UAAA,CACzB,GACDM,EAAON,CAAK,GACZb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAAA,QAAA;AAAA,MAC/B,CACD,EACA,MAAM,MAAM;AACL,cAAAU,IAAQ,IAAI,MAAM,+BAA+B;AACvD,QAAAM,EAAON,CAAK,GACZb,EAAW,UAAU,MACrBC,EAAU,UAAU,MACpBE,EAAmB,UAAU;AAAA,MAAA,CAC9B;AAAA,IAAA,CACJ;AAAA,IAEH;AAAA,MACES;AAAA,MACAJ;AAAA,MACAJ;AAAA,MACAE;AAAA,IAAA;AAAA,EAEJ;AAEA,EAAAkB,EAAU,MACD,MAAM;AACU,IAAAZ,EAAA,GACAJ,EAAA;AAAA,EACvB,GACC,CAACI,GAAsBJ,CAAoB,CAAC;AAE/C,QAAMiB,IAAwCC;AAAA,IAC5C,OAAO;AAAA,MACL,YAAAL;AAAA,MACA,aAAAJ;AAAA,IAAA;AAAA,IAEF,CAACI,GAAYJ,CAAW;AAAA,EAC1B;AAEA,SACG,gBAAAU,EAAAtC,EAAmB,UAAnB,EAA4B,OAAOoC,GACjC,UAAA;AAAA,IAAA9B;AAAA,IACD,gBAAAiC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAKhC;AAAA,QACL,MAAK;AAAA,QACL,QAAO;AAAA,QACP,UAAUkB;AAAA,QACV,OAAO,EAAC,SAAS,OAAM;AAAA,QACvB,eAAY;AAAA,MAAA;AAAA,IACd;AAAA,IACA,gBAAAc;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK9B;AAAA,QACL,MAAK;AAAA,QACL,QAAO;AAAA,QACP,SAAQ;AAAA,QACR,UAAUgB;AAAA,QACV,OAAO,EAAC,SAAS,OAAM;AAAA,QACvB,eAAY;AAAA,MAAA;AAAA,IACd;AAAA,IACA,gBAAAc;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK7B;AAAA,QACL,MAAK;AAAA,QACL,QAAO;AAAA,QACP,SAAQ;AAAA,QACR,UAAUe;AAAA,QACV,OAAO,EAAC,SAAS,OAAM;AAAA,QACvB,eAAY;AAAA,MAAA;AAAA,IAAA;AAAA,EACd,GACF;AAEJ;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getWindowLocationPathname.js","sources":["../../src/utils/getWindowLocationPathname.ts"],"sourcesContent":["export function getWindowLocationPathname(): string {\n if (typeof window !== 'undefined' && window.location) {\n return window.location.pathname || '/'\n }\n return ''\n}\n"],"names":["getWindowLocationPathname"],"mappings":"AAAO,SAASA,IAAoC;AAClD,SAAI,OAAO,SAAW,OAAe,OAAO,WACnC,OAAO,SAAS,YAAY,MAE9B;AACT;"}
|
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.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@playwright/test": "^1.54.2",
|
|
82
|
-
"@shopify/generate-docs": "^0.16.6",
|
|
83
82
|
"@storybook/addon-docs": "^9.0.16",
|
|
84
83
|
"@storybook/react-vite": "^9.0.16",
|
|
85
84
|
"@testing-library/jest-dom": "^6.6.4",
|
|
@@ -17,6 +17,14 @@ vi.mock('../hooks/util/useRequestPermissions', () => ({
|
|
|
17
17
|
}),
|
|
18
18
|
}))
|
|
19
19
|
|
|
20
|
+
// Mock useReportInteraction hook
|
|
21
|
+
const mockReportInteraction = vi.fn()
|
|
22
|
+
vi.mock('../internal/useReportInteraction', () => ({
|
|
23
|
+
useReportInteraction: () => ({
|
|
24
|
+
reportInteraction: mockReportInteraction,
|
|
25
|
+
}),
|
|
26
|
+
}))
|
|
27
|
+
|
|
20
28
|
describe('ImagePickerProvider', () => {
|
|
21
29
|
const originalMinisParams = window.minisParams
|
|
22
30
|
|
|
@@ -24,6 +32,8 @@ describe('ImagePickerProvider', () => {
|
|
|
24
32
|
vi.clearAllMocks()
|
|
25
33
|
// Default to granting permission
|
|
26
34
|
mockRequestPermission.mockResolvedValue({granted: true})
|
|
35
|
+
// Clear interaction reporting mock
|
|
36
|
+
mockReportInteraction.mockClear()
|
|
27
37
|
})
|
|
28
38
|
|
|
29
39
|
afterEach(() => {
|
|
@@ -716,4 +726,385 @@ describe('ImagePickerProvider', () => {
|
|
|
716
726
|
expect(galleryInput.value).toBe('')
|
|
717
727
|
})
|
|
718
728
|
})
|
|
729
|
+
|
|
730
|
+
describe('Interaction Reporting', () => {
|
|
731
|
+
describe('Gallery interactions', () => {
|
|
732
|
+
it('reports image_picker_open when gallery is opened', async () => {
|
|
733
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
734
|
+
|
|
735
|
+
const TestComponent = () => {
|
|
736
|
+
const {openGallery} = useImagePickerContext()
|
|
737
|
+
|
|
738
|
+
return (
|
|
739
|
+
<button
|
|
740
|
+
type="button"
|
|
741
|
+
onClick={() =>
|
|
742
|
+
openGallery().catch(() => {
|
|
743
|
+
// Ignore errors from cleanup
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
>
|
|
747
|
+
Open Gallery
|
|
748
|
+
</button>
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
render(
|
|
753
|
+
<ImagePickerProvider>
|
|
754
|
+
<TestComponent />
|
|
755
|
+
</ImagePickerProvider>
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
const button = screen.getByText('Open Gallery')
|
|
759
|
+
fireEvent.click(button)
|
|
760
|
+
|
|
761
|
+
// Wait for interaction to be reported
|
|
762
|
+
await vi.waitFor(() => {
|
|
763
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
764
|
+
interactionType: 'image_picker_open',
|
|
765
|
+
})
|
|
766
|
+
})
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
it('reports image_picker_success when file is selected', async () => {
|
|
770
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
771
|
+
|
|
772
|
+
const TestComponent = () => {
|
|
773
|
+
const {openGallery} = useImagePickerContext()
|
|
774
|
+
|
|
775
|
+
return (
|
|
776
|
+
<button
|
|
777
|
+
type="button"
|
|
778
|
+
onClick={() =>
|
|
779
|
+
openGallery().catch(() => {
|
|
780
|
+
// Ignore errors from cleanup
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
>
|
|
784
|
+
Open Gallery
|
|
785
|
+
</button>
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const {container} = render(
|
|
790
|
+
<ImagePickerProvider>
|
|
791
|
+
<TestComponent />
|
|
792
|
+
</ImagePickerProvider>
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
const galleryInput = container.querySelector(
|
|
796
|
+
'input[type="file"]:not([capture])'
|
|
797
|
+
) as HTMLInputElement
|
|
798
|
+
|
|
799
|
+
const button = screen.getByText('Open Gallery')
|
|
800
|
+
fireEvent.click(button)
|
|
801
|
+
|
|
802
|
+
// Wait for permission and initial interaction
|
|
803
|
+
await vi.waitFor(() => {
|
|
804
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
805
|
+
interactionType: 'image_picker_open',
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
// Clear mock to check only for success interaction
|
|
810
|
+
mockReportInteraction.mockClear()
|
|
811
|
+
|
|
812
|
+
// Simulate file selection
|
|
813
|
+
const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
814
|
+
Object.defineProperty(galleryInput, 'files', {
|
|
815
|
+
value: [file],
|
|
816
|
+
configurable: true,
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
await act(async () => {
|
|
820
|
+
fireEvent.change(galleryInput)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
// Check success was reported
|
|
824
|
+
await vi.waitFor(() => {
|
|
825
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
826
|
+
interactionType: 'image_picker_success',
|
|
827
|
+
})
|
|
828
|
+
})
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('reports image_picker_error when interrupted by new picker', async () => {
|
|
832
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
833
|
+
|
|
834
|
+
const TestComponent = () => {
|
|
835
|
+
const {openGallery, openCamera} = useImagePickerContext()
|
|
836
|
+
|
|
837
|
+
return (
|
|
838
|
+
<>
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
onClick={() =>
|
|
842
|
+
openGallery().catch(() => {
|
|
843
|
+
// Expected to fail
|
|
844
|
+
})
|
|
845
|
+
}
|
|
846
|
+
>
|
|
847
|
+
Open Gallery
|
|
848
|
+
</button>
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
onClick={() =>
|
|
852
|
+
openCamera().catch(() => {
|
|
853
|
+
// Ignore errors
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
>
|
|
857
|
+
Open Camera
|
|
858
|
+
</button>
|
|
859
|
+
</>
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
render(
|
|
864
|
+
<ImagePickerProvider>
|
|
865
|
+
<TestComponent />
|
|
866
|
+
</ImagePickerProvider>
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
// Open gallery first
|
|
870
|
+
const galleryButton = screen.getByText('Open Gallery')
|
|
871
|
+
fireEvent.click(galleryButton)
|
|
872
|
+
|
|
873
|
+
// Wait for gallery open to be reported
|
|
874
|
+
await vi.waitFor(() => {
|
|
875
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
876
|
+
interactionType: 'image_picker_open',
|
|
877
|
+
})
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
mockReportInteraction.mockClear()
|
|
881
|
+
|
|
882
|
+
// Open camera to interrupt gallery
|
|
883
|
+
const cameraButton = screen.getByText('Open Camera')
|
|
884
|
+
fireEvent.click(cameraButton)
|
|
885
|
+
|
|
886
|
+
// Check error was reported for interrupted gallery operation
|
|
887
|
+
await vi.waitFor(() => {
|
|
888
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
889
|
+
interactionType: 'image_picker_error',
|
|
890
|
+
interactionValue:
|
|
891
|
+
'New file picker opened before previous completed',
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
})
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
describe('Camera interactions', () => {
|
|
898
|
+
it('reports camera_open when camera is opened', async () => {
|
|
899
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
900
|
+
|
|
901
|
+
const TestComponent = () => {
|
|
902
|
+
const {openCamera} = useImagePickerContext()
|
|
903
|
+
|
|
904
|
+
return (
|
|
905
|
+
<button
|
|
906
|
+
type="button"
|
|
907
|
+
onClick={() =>
|
|
908
|
+
openCamera().catch(() => {
|
|
909
|
+
// Ignore errors from cleanup
|
|
910
|
+
})
|
|
911
|
+
}
|
|
912
|
+
>
|
|
913
|
+
Open Camera
|
|
914
|
+
</button>
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
render(
|
|
919
|
+
<ImagePickerProvider>
|
|
920
|
+
<TestComponent />
|
|
921
|
+
</ImagePickerProvider>
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
const button = screen.getByText('Open Camera')
|
|
925
|
+
fireEvent.click(button)
|
|
926
|
+
|
|
927
|
+
// Wait for interaction to be reported
|
|
928
|
+
await vi.waitFor(() => {
|
|
929
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
930
|
+
interactionType: 'camera_open',
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('reports camera_success when photo is captured', async () => {
|
|
936
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
937
|
+
|
|
938
|
+
const TestComponent = () => {
|
|
939
|
+
const {openCamera} = useImagePickerContext()
|
|
940
|
+
|
|
941
|
+
return (
|
|
942
|
+
<button
|
|
943
|
+
type="button"
|
|
944
|
+
onClick={() =>
|
|
945
|
+
openCamera().catch(() => {
|
|
946
|
+
// Ignore errors from cleanup
|
|
947
|
+
})
|
|
948
|
+
}
|
|
949
|
+
>
|
|
950
|
+
Open Camera
|
|
951
|
+
</button>
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const {container} = render(
|
|
956
|
+
<ImagePickerProvider>
|
|
957
|
+
<TestComponent />
|
|
958
|
+
</ImagePickerProvider>
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
const cameraInput = container.querySelector(
|
|
962
|
+
'input[capture="environment"]'
|
|
963
|
+
) as HTMLInputElement
|
|
964
|
+
|
|
965
|
+
const button = screen.getByText('Open Camera')
|
|
966
|
+
fireEvent.click(button)
|
|
967
|
+
|
|
968
|
+
// Wait for camera open to be reported
|
|
969
|
+
await vi.waitFor(() => {
|
|
970
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
971
|
+
interactionType: 'camera_open',
|
|
972
|
+
})
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
mockReportInteraction.mockClear()
|
|
976
|
+
|
|
977
|
+
// Simulate photo capture
|
|
978
|
+
const file = new File(['photo'], 'photo.jpg', {type: 'image/jpeg'})
|
|
979
|
+
Object.defineProperty(cameraInput, 'files', {
|
|
980
|
+
value: [file],
|
|
981
|
+
configurable: true,
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
await act(async () => {
|
|
985
|
+
fireEvent.change(cameraInput)
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
// Check success was reported
|
|
989
|
+
await vi.waitFor(() => {
|
|
990
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
991
|
+
interactionType: 'camera_success',
|
|
992
|
+
})
|
|
993
|
+
})
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
it('reports camera_error when interrupted by new picker', async () => {
|
|
997
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
998
|
+
|
|
999
|
+
const TestComponent = () => {
|
|
1000
|
+
const {openCamera, openGallery} = useImagePickerContext()
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<>
|
|
1004
|
+
<button
|
|
1005
|
+
type="button"
|
|
1006
|
+
onClick={() =>
|
|
1007
|
+
openCamera().catch(() => {
|
|
1008
|
+
// Expected to fail
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
>
|
|
1012
|
+
Open Camera
|
|
1013
|
+
</button>
|
|
1014
|
+
<button
|
|
1015
|
+
type="button"
|
|
1016
|
+
onClick={() =>
|
|
1017
|
+
openGallery().catch(() => {
|
|
1018
|
+
// Ignore errors
|
|
1019
|
+
})
|
|
1020
|
+
}
|
|
1021
|
+
>
|
|
1022
|
+
Open Gallery
|
|
1023
|
+
</button>
|
|
1024
|
+
</>
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
render(
|
|
1029
|
+
<ImagePickerProvider>
|
|
1030
|
+
<TestComponent />
|
|
1031
|
+
</ImagePickerProvider>
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
// Open camera first
|
|
1035
|
+
const cameraButton = screen.getByText('Open Camera')
|
|
1036
|
+
fireEvent.click(cameraButton)
|
|
1037
|
+
|
|
1038
|
+
// Wait for camera open to be reported
|
|
1039
|
+
await vi.waitFor(() => {
|
|
1040
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1041
|
+
interactionType: 'camera_open',
|
|
1042
|
+
})
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
mockReportInteraction.mockClear()
|
|
1046
|
+
|
|
1047
|
+
// Open gallery to interrupt camera
|
|
1048
|
+
const galleryButton = screen.getByText('Open Gallery')
|
|
1049
|
+
fireEvent.click(galleryButton)
|
|
1050
|
+
|
|
1051
|
+
// Check error was reported for interrupted camera operation
|
|
1052
|
+
await vi.waitFor(() => {
|
|
1053
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1054
|
+
interactionType: 'camera_error',
|
|
1055
|
+
interactionValue:
|
|
1056
|
+
'New file picker opened before previous completed',
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
it('reports camera_error when permission is denied', async () => {
|
|
1062
|
+
mockRequestPermission.mockResolvedValue({granted: false})
|
|
1063
|
+
|
|
1064
|
+
const TestComponent = () => {
|
|
1065
|
+
const {openCamera} = useImagePickerContext()
|
|
1066
|
+
|
|
1067
|
+
return (
|
|
1068
|
+
<button
|
|
1069
|
+
type="button"
|
|
1070
|
+
onClick={() =>
|
|
1071
|
+
openCamera().catch(() => {
|
|
1072
|
+
// Expected to fail
|
|
1073
|
+
})
|
|
1074
|
+
}
|
|
1075
|
+
>
|
|
1076
|
+
Open Camera
|
|
1077
|
+
</button>
|
|
1078
|
+
)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
render(
|
|
1082
|
+
<ImagePickerProvider>
|
|
1083
|
+
<TestComponent />
|
|
1084
|
+
</ImagePickerProvider>
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
const button = screen.getByText('Open Camera')
|
|
1088
|
+
fireEvent.click(button)
|
|
1089
|
+
|
|
1090
|
+
// Wait for permission request
|
|
1091
|
+
await vi.waitFor(() => {
|
|
1092
|
+
expect(mockRequestPermission).toHaveBeenCalled()
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
// Camera open should be reported
|
|
1096
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1097
|
+
interactionType: 'camera_open',
|
|
1098
|
+
})
|
|
1099
|
+
|
|
1100
|
+
// Error interaction should be reported for permission denial
|
|
1101
|
+
await vi.waitFor(() => {
|
|
1102
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1103
|
+
interactionType: 'camera_error',
|
|
1104
|
+
interactionValue: 'Camera permission not granted',
|
|
1105
|
+
})
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
})
|
|
1109
|
+
})
|
|
719
1110
|
})
|
|
@@ -8,6 +8,7 @@ import React, {
|
|
|
8
8
|
} from 'react'
|
|
9
9
|
|
|
10
10
|
import {useRequestPermissions} from '../hooks/util/useRequestPermissions'
|
|
11
|
+
import {useReportInteraction} from '../internal/useReportInteraction'
|
|
11
12
|
|
|
12
13
|
export type CameraFacing = 'front' | 'back'
|
|
13
14
|
|
|
@@ -42,8 +43,10 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
42
43
|
input: HTMLInputElement
|
|
43
44
|
handler: () => void
|
|
44
45
|
} | null>(null)
|
|
46
|
+
const activeOperationRef = useRef<'gallery' | 'camera' | null>(null)
|
|
45
47
|
|
|
46
48
|
const {requestPermission} = useRequestPermissions()
|
|
49
|
+
const {reportInteraction} = useReportInteraction()
|
|
47
50
|
|
|
48
51
|
const cleanupCancelHandler = useCallback(() => {
|
|
49
52
|
if (activeCancelHandlerRef.current) {
|
|
@@ -55,30 +58,56 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
55
58
|
|
|
56
59
|
const rejectPendingPromise = useCallback(() => {
|
|
57
60
|
if (rejectRef.current) {
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const error = new Error(
|
|
62
|
+
'New file picker opened before previous completed'
|
|
60
63
|
)
|
|
64
|
+
if (activeOperationRef.current === 'gallery') {
|
|
65
|
+
reportInteraction({
|
|
66
|
+
interactionType: 'image_picker_error',
|
|
67
|
+
interactionValue: error.message,
|
|
68
|
+
})
|
|
69
|
+
} else if (activeOperationRef.current === 'camera') {
|
|
70
|
+
reportInteraction({
|
|
71
|
+
interactionType: 'camera_error',
|
|
72
|
+
interactionValue: error.message,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
rejectRef.current(error)
|
|
61
77
|
resolveRef.current = null
|
|
62
78
|
rejectRef.current = null
|
|
79
|
+
activeOperationRef.current = null
|
|
63
80
|
}
|
|
64
|
-
}, [])
|
|
81
|
+
}, [reportInteraction])
|
|
65
82
|
|
|
66
83
|
const handleFileChange = useCallback(
|
|
67
84
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
68
85
|
const file = event.target.files?.[0]
|
|
69
86
|
|
|
70
87
|
if (file && resolveRef.current) {
|
|
88
|
+
// Report success based on the active operation
|
|
89
|
+
if (activeOperationRef.current === 'gallery') {
|
|
90
|
+
reportInteraction({
|
|
91
|
+
interactionType: 'image_picker_success',
|
|
92
|
+
})
|
|
93
|
+
} else if (activeOperationRef.current === 'camera') {
|
|
94
|
+
reportInteraction({
|
|
95
|
+
interactionType: 'camera_success',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
71
99
|
resolveRef.current(file)
|
|
72
100
|
|
|
73
101
|
resolveRef.current = null
|
|
74
102
|
rejectRef.current = null
|
|
103
|
+
activeOperationRef.current = null
|
|
75
104
|
|
|
76
105
|
cleanupCancelHandler()
|
|
77
106
|
}
|
|
78
107
|
|
|
79
108
|
event.target.value = ''
|
|
80
109
|
},
|
|
81
|
-
[cleanupCancelHandler]
|
|
110
|
+
[cleanupCancelHandler, reportInteraction]
|
|
82
111
|
)
|
|
83
112
|
|
|
84
113
|
const openGallery = useCallback(() => {
|
|
@@ -88,21 +117,34 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
88
117
|
|
|
89
118
|
resolveRef.current = resolve
|
|
90
119
|
rejectRef.current = reject
|
|
120
|
+
activeOperationRef.current = 'gallery'
|
|
91
121
|
|
|
92
122
|
const input = galleryInputRef.current
|
|
93
123
|
|
|
94
124
|
if (!input) {
|
|
95
|
-
|
|
125
|
+
const error = new Error('Gallery input not found')
|
|
126
|
+
reportInteraction({
|
|
127
|
+
interactionType: 'image_picker_error',
|
|
128
|
+
interactionValue: error.message,
|
|
129
|
+
})
|
|
130
|
+
reject(error)
|
|
96
131
|
resolveRef.current = null
|
|
97
132
|
rejectRef.current = null
|
|
133
|
+
activeOperationRef.current = null
|
|
98
134
|
return
|
|
99
135
|
}
|
|
100
136
|
|
|
101
137
|
const handleCancel = () => {
|
|
102
138
|
if (rejectRef.current) {
|
|
103
|
-
|
|
139
|
+
const error = new Error('User cancelled file selection')
|
|
140
|
+
reportInteraction({
|
|
141
|
+
interactionType: 'image_picker_error',
|
|
142
|
+
interactionValue: error.message,
|
|
143
|
+
})
|
|
144
|
+
rejectRef.current(error)
|
|
104
145
|
resolveRef.current = null
|
|
105
146
|
rejectRef.current = null
|
|
147
|
+
activeOperationRef.current = null
|
|
106
148
|
}
|
|
107
149
|
cleanupCancelHandler()
|
|
108
150
|
}
|
|
@@ -110,6 +152,10 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
110
152
|
input.addEventListener('cancel', handleCancel)
|
|
111
153
|
activeCancelHandlerRef.current = {input, handler: handleCancel}
|
|
112
154
|
|
|
155
|
+
reportInteraction({
|
|
156
|
+
interactionType: 'image_picker_open',
|
|
157
|
+
})
|
|
158
|
+
|
|
113
159
|
requestPermission({permission: 'CAMERA'})
|
|
114
160
|
.then(() => {
|
|
115
161
|
// This will show both Camera and Gallery
|
|
@@ -120,7 +166,12 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
120
166
|
input.click()
|
|
121
167
|
})
|
|
122
168
|
})
|
|
123
|
-
}, [
|
|
169
|
+
}, [
|
|
170
|
+
rejectPendingPromise,
|
|
171
|
+
cleanupCancelHandler,
|
|
172
|
+
requestPermission,
|
|
173
|
+
reportInteraction,
|
|
174
|
+
])
|
|
124
175
|
|
|
125
176
|
const openCamera = useCallback(
|
|
126
177
|
(cameraFacing: CameraFacing = 'back') => {
|
|
@@ -130,6 +181,7 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
130
181
|
|
|
131
182
|
resolveRef.current = resolve
|
|
132
183
|
rejectRef.current = reject
|
|
184
|
+
activeOperationRef.current = 'camera'
|
|
133
185
|
|
|
134
186
|
const input =
|
|
135
187
|
cameraFacing === 'front'
|
|
@@ -137,17 +189,29 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
137
189
|
: backCameraInputRef.current
|
|
138
190
|
|
|
139
191
|
if (!input) {
|
|
140
|
-
|
|
192
|
+
const error = new Error('Camera input not found')
|
|
193
|
+
reportInteraction({
|
|
194
|
+
interactionType: 'camera_error',
|
|
195
|
+
interactionValue: error.message,
|
|
196
|
+
})
|
|
197
|
+
reject(error)
|
|
141
198
|
resolveRef.current = null
|
|
142
199
|
rejectRef.current = null
|
|
200
|
+
activeOperationRef.current = null
|
|
143
201
|
return
|
|
144
202
|
}
|
|
145
203
|
|
|
146
204
|
const handleCancel = () => {
|
|
147
205
|
if (rejectRef.current) {
|
|
148
|
-
|
|
206
|
+
const error = new Error('User cancelled camera')
|
|
207
|
+
reportInteraction({
|
|
208
|
+
interactionType: 'camera_error',
|
|
209
|
+
interactionValue: error.message,
|
|
210
|
+
})
|
|
211
|
+
rejectRef.current(error)
|
|
149
212
|
resolveRef.current = null
|
|
150
213
|
rejectRef.current = null
|
|
214
|
+
activeOperationRef.current = null
|
|
151
215
|
}
|
|
152
216
|
cleanupCancelHandler()
|
|
153
217
|
}
|
|
@@ -155,24 +219,41 @@ export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
|
155
219
|
input.addEventListener('cancel', handleCancel)
|
|
156
220
|
activeCancelHandlerRef.current = {input, handler: handleCancel}
|
|
157
221
|
|
|
222
|
+
reportInteraction({
|
|
223
|
+
interactionType: 'camera_open',
|
|
224
|
+
})
|
|
225
|
+
|
|
158
226
|
requestPermission({permission: 'CAMERA'})
|
|
159
227
|
.then(({granted}) => {
|
|
160
228
|
if (granted) {
|
|
161
229
|
input.click()
|
|
162
230
|
} else {
|
|
163
|
-
|
|
231
|
+
const error = new Error('Camera permission not granted')
|
|
232
|
+
reportInteraction({
|
|
233
|
+
interactionType: 'camera_error',
|
|
234
|
+
interactionValue: error.message,
|
|
235
|
+
})
|
|
236
|
+
reject(error)
|
|
164
237
|
resolveRef.current = null
|
|
165
238
|
rejectRef.current = null
|
|
239
|
+
activeOperationRef.current = null
|
|
166
240
|
}
|
|
167
241
|
})
|
|
168
242
|
.catch(() => {
|
|
169
|
-
|
|
243
|
+
const error = new Error('Camera permission not granted')
|
|
244
|
+
reject(error)
|
|
170
245
|
resolveRef.current = null
|
|
171
246
|
rejectRef.current = null
|
|
247
|
+
activeOperationRef.current = null
|
|
172
248
|
})
|
|
173
249
|
})
|
|
174
250
|
},
|
|
175
|
-
[
|
|
251
|
+
[
|
|
252
|
+
rejectPendingPromise,
|
|
253
|
+
cleanupCancelHandler,
|
|
254
|
+
requestPermission,
|
|
255
|
+
reportInteraction,
|
|
256
|
+
]
|
|
176
257
|
)
|
|
177
258
|
|
|
178
259
|
useEffect(() => {
|