@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.
@@ -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 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";
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
- const k = b(null);
5
- function H() {
6
- const f = A(k);
7
- if (!f)
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 f;
12
+ return g;
12
13
  }
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(() => {
15
- if (a.current) {
16
- const { input: u, handler: c } = a.current;
17
- u.removeEventListener("cancel", c), a.current = null;
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
- }, []), o = s(() => {
20
- e.current && (e.current(
21
- new Error("New file picker opened before previous completed")
22
- ), 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 = "";
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
- ), E = s(() => new Promise((u, c) => {
30
- o(), r(), n.current = u, e.current = c;
31
- const t = C.current;
32
- if (!t) {
33
- c(new Error("Gallery input not found")), n.current = null, e.current = null;
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 l = () => {
37
- e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null), r();
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
- t.addEventListener("cancel", l), a.current = { input: t, handler: l }, p({ permission: "CAMERA" }).then(() => {
40
- t.click();
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
- t.click();
69
+ u.click();
43
70
  });
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;
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 w = () => {
53
- e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null), r();
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
- 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);
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
- t(new Error("Camera permission not granted")), n.current = null, e.current = null;
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
- [o, r, p]
115
+ [
116
+ m,
117
+ a,
118
+ y,
119
+ r
120
+ ]
62
121
  );
63
- R(() => () => {
64
- o(), r();
65
- }, [o, r]);
66
- const v = I(
122
+ I(() => () => {
123
+ m(), a();
124
+ }, [m, a]);
125
+ const T = V(
67
126
  () => ({
68
- openCamera: P,
69
- openGallery: E
127
+ openCamera: v,
128
+ openGallery: P
70
129
  }),
71
- [P, E]
130
+ [v, P]
72
131
  );
73
- return /* @__PURE__ */ x(k.Provider, { value: v, children: [
74
- f,
75
- /* @__PURE__ */ m(
132
+ return /* @__PURE__ */ x(R.Provider, { value: T, children: [
133
+ g,
134
+ /* @__PURE__ */ h(
76
135
  "input",
77
136
  {
78
- ref: C,
137
+ ref: k,
79
138
  type: "file",
80
139
  accept: "image/*",
81
- onChange: d,
140
+ onChange: C,
82
141
  style: { display: "none" },
83
142
  "aria-hidden": "true"
84
143
  }
85
144
  ),
86
- /* @__PURE__ */ m(
145
+ /* @__PURE__ */ h(
87
146
  "input",
88
147
  {
89
- ref: h,
148
+ ref: _,
90
149
  type: "file",
91
150
  accept: "image/*",
92
151
  capture: "user",
93
- onChange: d,
152
+ onChange: C,
94
153
  style: { display: "none" },
95
154
  "aria-hidden": "true"
96
155
  }
97
156
  ),
98
- /* @__PURE__ */ m(
157
+ /* @__PURE__ */ h(
99
158
  "input",
100
159
  {
101
- ref: g,
160
+ ref: E,
102
161
  type: "file",
103
162
  accept: "image/*",
104
163
  capture: "environment",
105
- onChange: d,
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
- U as ImagePickerProvider,
114
- H as useImagePickerContext
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":";;;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;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,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,GAE9Db,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;AAAA,EAAA,CACJ,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,GAE5Cb,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;AAAA,IAAA,CACJ;AAAA,IAEH,CAACQ,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,EAAAlC,EAAmB,UAAnB,EAA4B,OAAOgC,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'\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":"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"}
@@ -0,0 +1,7 @@
1
+ function n() {
2
+ return typeof window < "u" && window.location ? window.location.pathname || "/" : "";
3
+ }
4
+ export {
5
+ n as getWindowLocationPathname
6
+ };
7
+ //# sourceMappingURL=getWindowLocationPathname.js.map
@@ -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.1",
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
- rejectRef.current(
59
- new Error('New file picker opened before previous completed')
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
- reject(new Error('Gallery input not found'))
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
- rejectRef.current(new Error('User cancelled file selection'))
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
- }, [rejectPendingPromise, cleanupCancelHandler, requestPermission])
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
- reject(new Error('Camera input not found'))
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
- rejectRef.current(new Error('User cancelled camera'))
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
- reject(new Error('Camera permission not granted'))
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
- reject(new Error('Camera permission not granted'))
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
- [rejectPendingPromise, cleanupCancelHandler, requestPermission]
251
+ [
252
+ rejectPendingPromise,
253
+ cleanupCancelHandler,
254
+ requestPermission,
255
+ reportInteraction,
256
+ ]
176
257
  )
177
258
 
178
259
  useEffect(() => {