@shopify/shop-minis-react 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/index2.js +3 -2
- package/dist/_virtual/index2.js.map +1 -1
- package/dist/_virtual/index3.js +2 -3
- package/dist/_virtual/index3.js.map +1 -1
- package/dist/components/MinisContainer.js +12 -11
- package/dist/components/MinisContainer.js.map +1 -1
- package/dist/hooks/util/useImagePicker.js +12 -0
- package/dist/hooks/util/useImagePicker.js.map +1 -0
- package/dist/index.js +26 -24
- package/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
- package/dist/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
- package/dist/providers/ImagePickerProvider.js +107 -0
- package/dist/providers/ImagePickerProvider.js.map +1 -0
- package/package.json +1 -1
- package/src/components/MinisContainer.tsx +3 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/util/useImagePicker.doc.tsx +41 -0
- package/src/hooks/util/useImagePicker.example.tsx +85 -0
- package/src/hooks/util/useImagePicker.tsx +18 -0
- package/src/providers/ImagePickerProvider.tsx +198 -0
package/dist/_virtual/index2.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index2.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
package/dist/_virtual/index3.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index3.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index3.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { jsx as
|
|
1
|
+
import { jsx as r, jsxs as c } from "react/jsx-runtime";
|
|
2
2
|
import { useState as m, useEffect as l } from "react";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { ImagePickerProvider as u } from "../providers/ImagePickerProvider.js";
|
|
4
|
+
function p({ children: s }) {
|
|
5
|
+
const [a, t] = m(!1);
|
|
5
6
|
return l(() => {
|
|
6
|
-
const n = () => window.minisSDK ? (
|
|
7
|
+
const n = () => window.minisSDK ? (t(!0), !0) : !1;
|
|
7
8
|
if (n())
|
|
8
9
|
return;
|
|
9
|
-
const e = (
|
|
10
|
-
const { type:
|
|
11
|
-
|
|
10
|
+
const e = (o) => {
|
|
11
|
+
const { type: d } = JSON.parse(o.data);
|
|
12
|
+
d === "MINIS_SDK_READY" && t(!0);
|
|
12
13
|
};
|
|
13
14
|
window.addEventListener("message", e), document.addEventListener("message", e);
|
|
14
15
|
const i = setInterval(() => {
|
|
@@ -17,12 +18,12 @@ function v({ children: s }) {
|
|
|
17
18
|
return () => {
|
|
18
19
|
window.removeEventListener("message", e), document.removeEventListener("message", e);
|
|
19
20
|
};
|
|
20
|
-
}, []), a ? s : /* @__PURE__ */
|
|
21
|
-
/* @__PURE__ */
|
|
22
|
-
/* @__PURE__ */
|
|
21
|
+
}, []), a ? /* @__PURE__ */ r(u, { children: s }) : /* @__PURE__ */ r("div", { className: "h-screen bg-gray-50 flex items-center justify-center", children: /* @__PURE__ */ c("div", { className: "text-center", children: [
|
|
22
|
+
/* @__PURE__ */ r("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4" }),
|
|
23
|
+
/* @__PURE__ */ r("p", { className: "text-gray-600", children: "Loading..." })
|
|
23
24
|
] }) });
|
|
24
25
|
}
|
|
25
26
|
export {
|
|
26
|
-
|
|
27
|
+
p as MinisContainer
|
|
27
28
|
};
|
|
28
29
|
//# sourceMappingURL=MinisContainer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MinisContainer.js","sources":["../../src/components/MinisContainer.tsx"],"sourcesContent":["import React, {useEffect, useState} from 'react'\n\nexport function MinisContainer({children}: {children: React.ReactNode}) {\n const [isSDKReady, setIsSDKReady] = useState(false)\n\n useEffect(() => {\n // Function to check if SDK is ready\n const checkSDKReady = () => {\n if (window.minisSDK) {\n setIsSDKReady(true)\n return true\n }\n return false\n }\n\n // Check immediately\n if (checkSDKReady()) {\n return\n }\n\n // If not ready, set up a listener for the MINIS_SDK_READY event\n const handleSDKReady = (event: any) => {\n const {type} = JSON.parse(event.data)\n\n if (type === 'MINIS_SDK_READY') {\n setIsSDKReady(true)\n }\n }\n\n // Listen for the MINIS_SDK_READY event\n window.addEventListener('message', handleSDKReady)\n document.addEventListener('message', handleSDKReady)\n\n // Also poll for SDK availability as a fallback\n const pollInterval = setInterval(() => {\n if (checkSDKReady()) {\n clearInterval(pollInterval)\n }\n }, 100)\n\n // Cleanup\n return () => {\n // clearInterval(pollInterval);\n window.removeEventListener('message', handleSDKReady)\n document.removeEventListener('message', handleSDKReady)\n }\n }, [])\n\n // Don't render anything until SDK is ready\n if (!isSDKReady) {\n return (\n <div className=\"h-screen bg-gray-50 flex items-center justify-center\">\n <div className=\"text-center\">\n <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4\" />\n <p className=\"text-gray-600\">Loading...</p>\n </div>\n </div>\n )\n }\n\n return children
|
|
1
|
+
{"version":3,"file":"MinisContainer.js","sources":["../../src/components/MinisContainer.tsx"],"sourcesContent":["import React, {useEffect, useState} from 'react'\n\nimport {ImagePickerProvider} from '../providers/ImagePickerProvider'\n\nexport function MinisContainer({children}: {children: React.ReactNode}) {\n const [isSDKReady, setIsSDKReady] = useState(false)\n\n useEffect(() => {\n // Function to check if SDK is ready\n const checkSDKReady = () => {\n if (window.minisSDK) {\n setIsSDKReady(true)\n return true\n }\n return false\n }\n\n // Check immediately\n if (checkSDKReady()) {\n return\n }\n\n // If not ready, set up a listener for the MINIS_SDK_READY event\n const handleSDKReady = (event: any) => {\n const {type} = JSON.parse(event.data)\n\n if (type === 'MINIS_SDK_READY') {\n setIsSDKReady(true)\n }\n }\n\n // Listen for the MINIS_SDK_READY event\n window.addEventListener('message', handleSDKReady)\n document.addEventListener('message', handleSDKReady)\n\n // Also poll for SDK availability as a fallback\n const pollInterval = setInterval(() => {\n if (checkSDKReady()) {\n clearInterval(pollInterval)\n }\n }, 100)\n\n // Cleanup\n return () => {\n // clearInterval(pollInterval);\n window.removeEventListener('message', handleSDKReady)\n document.removeEventListener('message', handleSDKReady)\n }\n }, [])\n\n // Don't render anything until SDK is ready\n if (!isSDKReady) {\n return (\n <div className=\"h-screen bg-gray-50 flex items-center justify-center\">\n <div className=\"text-center\">\n <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4\" />\n <p className=\"text-gray-600\">Loading...</p>\n </div>\n </div>\n )\n }\n\n return <ImagePickerProvider>{children}</ImagePickerProvider>\n}\n"],"names":["MinisContainer","children","isSDKReady","setIsSDKReady","useState","useEffect","checkSDKReady","handleSDKReady","event","type","pollInterval","jsx","ImagePickerProvider","jsxs"],"mappings":";;;AAIgB,SAAAA,EAAe,EAAC,UAAAC,KAAwC;AACtE,QAAM,CAACC,GAAYC,CAAa,IAAIC,EAAS,EAAK;AA8ClD,SA5CAC,EAAU,MAAM;AAEd,UAAMC,IAAgB,MAChB,OAAO,YACTH,EAAc,EAAI,GACX,MAEF;AAIT,QAAIG;AACF;AAII,UAAAC,IAAiB,CAACC,MAAe;AACrC,YAAM,EAAC,MAAAC,EAAI,IAAI,KAAK,MAAMD,EAAM,IAAI;AAEpC,MAAIC,MAAS,qBACXN,EAAc,EAAI;AAAA,IAEtB;AAGO,WAAA,iBAAiB,WAAWI,CAAc,GACxC,SAAA,iBAAiB,WAAWA,CAAc;AAG7C,UAAAG,IAAe,YAAY,MAAM;AACrC,MAAIJ,OACF,cAAcI,CAAY;AAAA,OAE3B,GAAG;AAGN,WAAO,MAAM;AAEJ,aAAA,oBAAoB,WAAWH,CAAc,GAC3C,SAAA,oBAAoB,WAAWA,CAAc;AAAA,IACxD;AAAA,EACF,GAAG,EAAE,GAGAL,IAWE,gBAAAS,EAACC,KAAqB,UAAAX,GAAS,sBATjC,OAAI,EAAA,WAAU,wDACb,UAAC,gBAAAY,EAAA,OAAA,EAAI,WAAU,eACb,UAAA;AAAA,IAAC,gBAAAF,EAAA,OAAA,EAAI,WAAU,4EAA4E,CAAA;AAAA,IAC1F,gBAAAA,EAAA,KAAA,EAAE,WAAU,iBAAgB,UAAU,aAAA,CAAA;AAAA,EAAA,EAAA,CACzC,EACF,CAAA;AAKN;"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useImagePickerContext as o } from "../../providers/ImagePickerProvider.js";
|
|
2
|
+
function t() {
|
|
3
|
+
const { openCamera: e, openGallery: r } = o();
|
|
4
|
+
return {
|
|
5
|
+
openCamera: e,
|
|
6
|
+
openGallery: r
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
t as useImagePicker
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=useImagePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useImagePicker.js","sources":["../../../src/hooks/util/useImagePicker.tsx"],"sourcesContent":["import {\n CameraFacing,\n useImagePickerContext,\n} from '../../providers/ImagePickerProvider'\n\ninterface UseImagePickerReturn {\n openCamera: (cameraFacing?: CameraFacing) => Promise<File>\n openGallery: () => Promise<File>\n}\n\nexport function useImagePicker(): UseImagePickerReturn {\n const {openCamera, openGallery} = useImagePickerContext()\n\n return {\n openCamera,\n openGallery,\n }\n}\n"],"names":["useImagePicker","openCamera","openGallery","useImagePickerContext"],"mappings":";AAUO,SAASA,IAAuC;AACrD,QAAM,EAAC,YAAAC,GAAY,aAAAC,EAAW,IAAIC,EAAsB;AAEjD,SAAA;AAAA,IACL,YAAAF;AAAA,IACA,aAAAC;AAAA,EACF;AACF;"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MinisContainer as o } from "./components/MinisContainer.js";
|
|
2
2
|
import { ProductCard as a, ProductCardBadge as i, ProductCardCurrentPrice as l, ProductCardFavoriteButton as n, ProductCardImage as c, ProductCardImageContainer as u, ProductCardInfo as d, ProductCardOriginalPrice as p, ProductCardPrice as s, ProductCardRoot as m, ProductCardTitle as f } from "./components/commerce/product-card.js";
|
|
3
|
-
import { ProductLink as g, ProductLinkActions as C, ProductLinkCurrentPrice as P, ProductLinkDiscountPrice as D, ProductLinkImage as S, ProductLinkInfo as A, ProductLinkOriginalPrice as
|
|
3
|
+
import { ProductLink as g, ProductLinkActions as C, ProductLinkCurrentPrice as P, ProductLinkDiscountPrice as D, ProductLinkImage as S, ProductLinkInfo as A, ProductLinkOriginalPrice as k, ProductLinkPrice as L, ProductLinkRating as T, ProductLinkRoot as h, ProductLinkTitle as w } from "./components/commerce/product-link.js";
|
|
4
4
|
import { Accordion as b, AccordionContent as v, AccordionItem as R, AccordionTrigger as F } from "./components/atoms/accordion.js";
|
|
5
5
|
import { Alert as y, AlertDescription as E, AlertTitle as H } from "./components/atoms/alert.js";
|
|
6
6
|
import { AlertDialog as G, AlertDialogAction as M, AlertDialogCancel as N, AlertDialogContent as U, AlertDialogDescription as z, AlertDialogFooter as V, AlertDialogHeader as j, AlertDialogOverlay as q, AlertDialogPortal as J, AlertDialogTitle as K, AlertDialogTrigger as Q } from "./components/atoms/alert-dialog.js";
|
|
@@ -10,7 +10,7 @@ import { Button as or, buttonVariants as tr } from "./components/atoms/button.js
|
|
|
10
10
|
import { Card as ir, CardAction as lr, CardContent as nr, CardDescription as cr, CardFooter as ur, CardHeader as dr, CardTitle as pr } from "./components/atoms/card.js";
|
|
11
11
|
import { Carousel as mr, CarouselContent as fr, CarouselItem as xr, CarouselNext as gr, CarouselPrevious as Cr } from "./components/atoms/carousel.js";
|
|
12
12
|
import { Checkbox as Dr } from "./components/atoms/checkbox.js";
|
|
13
|
-
import { Dialog as Ar, DialogClose as
|
|
13
|
+
import { Dialog as Ar, DialogClose as kr, DialogContent as Lr, DialogDescription as Tr, DialogFooter as hr, DialogHeader as wr, DialogOverlay as Ir, DialogPortal as br, DialogTitle as vr, DialogTrigger as Rr } from "./components/atoms/dialog.js";
|
|
14
14
|
import { Drawer as Br, DrawerClose as yr, DrawerContent as Er, DrawerDescription as Hr, DrawerFooter as Or, DrawerHeader as Gr, DrawerOverlay as Mr, DrawerPortal as Nr, DrawerTitle as Ur, DrawerTrigger as zr } from "./components/atoms/drawer.js";
|
|
15
15
|
import { Input as jr } from "./components/atoms/input.js";
|
|
16
16
|
import { Label as Jr } from "./components/atoms/label.js";
|
|
@@ -20,7 +20,7 @@ import { ResizableHandle as _r, ResizablePanel as $r, ResizablePanelGroup as re
|
|
|
20
20
|
import { ScrollArea as oe, ScrollBar as te } from "./components/atoms/scroll-area.js";
|
|
21
21
|
import { Select as ie, SelectContent as le, SelectGroup as ne, SelectItem as ce, SelectLabel as ue, SelectScrollDownButton as de, SelectScrollUpButton as pe, SelectSeparator as se, SelectTrigger as me, SelectValue as fe } from "./components/atoms/select.js";
|
|
22
22
|
import { Separator as ge } from "./components/atoms/separator.js";
|
|
23
|
-
import { Sheet as Pe, SheetClose as De, SheetContent as Se, SheetDescription as Ae, SheetFooter as
|
|
23
|
+
import { Sheet as Pe, SheetClose as De, SheetContent as Se, SheetDescription as Ae, SheetFooter as ke, SheetHeader as Le, SheetTitle as Te, SheetTrigger as he } from "./components/atoms/sheet.js";
|
|
24
24
|
import { Toaster as Ie } from "./components/atoms/sonner.js";
|
|
25
25
|
import { useSavedProductsActions as ve } from "./hooks/user/useSavedProductsActions.js";
|
|
26
26
|
import { useFollowedShopsActions as Fe } from "./hooks/user/useFollowedShopsActions.js";
|
|
@@ -43,9 +43,10 @@ import { useRecommendedShops as uo } from "./hooks/shop/useRecommendedShops.js";
|
|
|
43
43
|
import { useErrorToast as so } from "./hooks/util/useErrorToast.js";
|
|
44
44
|
import { useErrorScreen as fo } from "./hooks/util/useErrorScreen.js";
|
|
45
45
|
import { useShare as go } from "./hooks/util/useShare.js";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
46
|
+
import { useImagePicker as Po } from "./hooks/util/useImagePicker.js";
|
|
47
|
+
import { MiniEntityNotFoundError as So, MiniError as Ao, MiniNetworkError as ko, formatError as Lo } from "./utils/errors.js";
|
|
48
|
+
import { parseUrl as ho } from "./utils/parseUrl.js";
|
|
49
|
+
import { Consent as Io, ConsentStatus as bo, CurrencyCode as vo, Gender as Ro } from "./types/minisSDK.generated.d.js";
|
|
49
50
|
export {
|
|
50
51
|
b as Accordion,
|
|
51
52
|
v as AccordionContent,
|
|
@@ -83,13 +84,13 @@ export {
|
|
|
83
84
|
gr as CarouselNext,
|
|
84
85
|
Cr as CarouselPrevious,
|
|
85
86
|
Dr as Checkbox,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
Io as Consent,
|
|
88
|
+
bo as ConsentStatus,
|
|
89
|
+
vo as CurrencyCode,
|
|
89
90
|
Ar as Dialog,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
kr as DialogClose,
|
|
92
|
+
Lr as DialogContent,
|
|
93
|
+
Tr as DialogDescription,
|
|
93
94
|
hr as DialogFooter,
|
|
94
95
|
wr as DialogHeader,
|
|
95
96
|
Ir as DialogOverlay,
|
|
@@ -106,12 +107,12 @@ export {
|
|
|
106
107
|
Nr as DrawerPortal,
|
|
107
108
|
Ur as DrawerTitle,
|
|
108
109
|
zr as DrawerTrigger,
|
|
109
|
-
|
|
110
|
+
Ro as Gender,
|
|
110
111
|
jr as Input,
|
|
111
112
|
Jr as Label,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
So as MiniEntityNotFoundError,
|
|
114
|
+
Ao as MiniError,
|
|
115
|
+
ko as MiniNetworkError,
|
|
115
116
|
o as MinisContainer,
|
|
116
117
|
a as ProductCard,
|
|
117
118
|
i as ProductCardBadge,
|
|
@@ -130,9 +131,9 @@ export {
|
|
|
130
131
|
D as ProductLinkDiscountPrice,
|
|
131
132
|
S as ProductLinkImage,
|
|
132
133
|
A as ProductLinkInfo,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
k as ProductLinkOriginalPrice,
|
|
135
|
+
L as ProductLinkPrice,
|
|
136
|
+
T as ProductLinkRating,
|
|
136
137
|
h as ProductLinkRoot,
|
|
137
138
|
w as ProductLinkTitle,
|
|
138
139
|
Qr as Progress,
|
|
@@ -158,15 +159,15 @@ export {
|
|
|
158
159
|
De as SheetClose,
|
|
159
160
|
Se as SheetContent,
|
|
160
161
|
Ae as SheetDescription,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
ke as SheetFooter,
|
|
163
|
+
Le as SheetHeader,
|
|
164
|
+
Te as SheetTitle,
|
|
164
165
|
he as SheetTrigger,
|
|
165
166
|
Ie as Toaster,
|
|
166
167
|
rr as badgeVariants,
|
|
167
168
|
tr as buttonVariants,
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
Lo as formatError,
|
|
170
|
+
ho as parseUrl,
|
|
170
171
|
Xe as useAsyncStorage,
|
|
171
172
|
Ge as useBuyerAttributes,
|
|
172
173
|
to as useCloseMini,
|
|
@@ -175,6 +176,7 @@ export {
|
|
|
175
176
|
fo as useErrorScreen,
|
|
176
177
|
so as useErrorToast,
|
|
177
178
|
Fe as useFollowedShopsActions,
|
|
179
|
+
Po as useImagePicker,
|
|
178
180
|
$e as useImageUpload,
|
|
179
181
|
He as useOrders,
|
|
180
182
|
Qe as usePopularProducts,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsxs as w, jsx as p } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as o, useCallback as s, useEffect as x, useMemo as E, createContext as I, useContext as R } from "react";
|
|
3
|
+
const P = I(null);
|
|
4
|
+
function j() {
|
|
5
|
+
const f = R(P);
|
|
6
|
+
if (!f)
|
|
7
|
+
throw new Error(
|
|
8
|
+
"useImagePickerContext must be used within an ImagePickerProvider"
|
|
9
|
+
);
|
|
10
|
+
return f;
|
|
11
|
+
}
|
|
12
|
+
function G({ children: f }) {
|
|
13
|
+
const m = o(null), C = o(null), g = o(null), n = o(null), e = o(null), i = o(null), r = s(() => {
|
|
14
|
+
if (i.current) {
|
|
15
|
+
const { input: c, handler: t } = i.current;
|
|
16
|
+
c.removeEventListener("cancel", t), i.current = null;
|
|
17
|
+
}
|
|
18
|
+
}, []), a = s(() => {
|
|
19
|
+
e.current && (e.current(
|
|
20
|
+
new Error("New file picker opened before previous completed")
|
|
21
|
+
), n.current = null, e.current = null);
|
|
22
|
+
}, []), d = s(
|
|
23
|
+
(c) => {
|
|
24
|
+
const t = c.target.files?.[0];
|
|
25
|
+
t && n.current && (n.current(t), n.current = null, e.current = null, r()), c.target.value = "";
|
|
26
|
+
},
|
|
27
|
+
[r]
|
|
28
|
+
), h = s(() => new Promise((c, t) => {
|
|
29
|
+
a(), r(), n.current = c, e.current = t;
|
|
30
|
+
const u = m.current;
|
|
31
|
+
if (!u) {
|
|
32
|
+
t(new Error("Gallery input not found")), n.current = null, e.current = null;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const l = () => {
|
|
36
|
+
e.current && (e.current(new Error("User cancelled file selection")), n.current = null, e.current = null), r();
|
|
37
|
+
};
|
|
38
|
+
u.addEventListener("cancel", l), i.current = { input: u, handler: l }, u.click();
|
|
39
|
+
}), [a, r]), v = s(
|
|
40
|
+
(c = "back") => new Promise((t, u) => {
|
|
41
|
+
a(), r(), n.current = t, e.current = u;
|
|
42
|
+
const l = c === "front" ? C.current : g.current;
|
|
43
|
+
if (!l) {
|
|
44
|
+
u(new Error("Camera input not found")), n.current = null, e.current = null;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const y = () => {
|
|
48
|
+
e.current && (e.current(new Error("User cancelled camera")), n.current = null, e.current = null), r();
|
|
49
|
+
};
|
|
50
|
+
l.addEventListener("cancel", y), i.current = { input: l, handler: y }, l.click();
|
|
51
|
+
}),
|
|
52
|
+
[a, r]
|
|
53
|
+
);
|
|
54
|
+
x(() => () => {
|
|
55
|
+
a(), r();
|
|
56
|
+
}, [a, r]);
|
|
57
|
+
const k = E(
|
|
58
|
+
() => ({
|
|
59
|
+
openCamera: v,
|
|
60
|
+
openGallery: h
|
|
61
|
+
}),
|
|
62
|
+
[v, h]
|
|
63
|
+
);
|
|
64
|
+
return /* @__PURE__ */ w(P.Provider, { value: k, children: [
|
|
65
|
+
f,
|
|
66
|
+
/* @__PURE__ */ p(
|
|
67
|
+
"input",
|
|
68
|
+
{
|
|
69
|
+
ref: m,
|
|
70
|
+
type: "file",
|
|
71
|
+
accept: "image/*",
|
|
72
|
+
onChange: d,
|
|
73
|
+
style: { display: "none" },
|
|
74
|
+
"aria-hidden": "true"
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
/* @__PURE__ */ p(
|
|
78
|
+
"input",
|
|
79
|
+
{
|
|
80
|
+
ref: C,
|
|
81
|
+
type: "file",
|
|
82
|
+
accept: "image/*",
|
|
83
|
+
capture: "user",
|
|
84
|
+
onChange: d,
|
|
85
|
+
style: { display: "none" },
|
|
86
|
+
"aria-hidden": "true"
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
/* @__PURE__ */ p(
|
|
90
|
+
"input",
|
|
91
|
+
{
|
|
92
|
+
ref: g,
|
|
93
|
+
type: "file",
|
|
94
|
+
accept: "image/*",
|
|
95
|
+
capture: "environment",
|
|
96
|
+
onChange: d,
|
|
97
|
+
style: { display: "none" },
|
|
98
|
+
"aria-hidden": "true"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
G as ImagePickerProvider,
|
|
105
|
+
j as useImagePickerContext
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=ImagePickerProvider.js.map
|
|
@@ -0,0 +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\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 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 input.click()\n })\n }, [rejectPendingPromise, cleanupCancelHandler])\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 input.click()\n })\n },\n [rejectPendingPromise, cleanupCancelHandler]\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","cleanupCancelHandler","useCallback","input","handler","rejectPendingPromise","handleFileChange","event","file","openGallery","resolve","reject","handleCancel","openCamera","cameraFacing","useEffect","contextValue","useMemo","jsxs","jsx"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, {useEffect, useState} from 'react'
|
|
2
2
|
|
|
3
|
+
import {ImagePickerProvider} from '../providers/ImagePickerProvider'
|
|
4
|
+
|
|
3
5
|
export function MinisContainer({children}: {children: React.ReactNode}) {
|
|
4
6
|
const [isSDKReady, setIsSDKReady] = useState(false)
|
|
5
7
|
|
|
@@ -58,5 +60,5 @@ export function MinisContainer({children}: {children: React.ReactNode}) {
|
|
|
58
60
|
)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
return children
|
|
63
|
+
return <ImagePickerProvider>{children}</ImagePickerProvider>
|
|
62
64
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const useImagePickerDoc = {
|
|
2
|
+
name: 'useImagePicker',
|
|
3
|
+
category: 'Utility',
|
|
4
|
+
description:
|
|
5
|
+
'Hook for selecting images from camera or gallery in web-based mini apps',
|
|
6
|
+
params: [
|
|
7
|
+
{
|
|
8
|
+
name: 'options',
|
|
9
|
+
type: 'UseImagePickerOptions',
|
|
10
|
+
description: 'Configuration options for the image picker',
|
|
11
|
+
optional: true,
|
|
12
|
+
properties: [
|
|
13
|
+
{
|
|
14
|
+
name: 'cameraFacing',
|
|
15
|
+
type: "'user' | 'environment'",
|
|
16
|
+
description: 'Which camera to use when opening camera',
|
|
17
|
+
optional: true,
|
|
18
|
+
default: "'environment'",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
returns: [
|
|
24
|
+
{
|
|
25
|
+
name: 'openCamera',
|
|
26
|
+
type: '() => Promise<File>',
|
|
27
|
+
description: 'Function to open the camera for capturing an image',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'openGallery',
|
|
31
|
+
type: '() => Promise<File>',
|
|
32
|
+
description: 'Function to open the gallery for selecting an image',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'ImagePickerInputs',
|
|
36
|
+
type: 'React.FC',
|
|
37
|
+
description:
|
|
38
|
+
'Component that renders hidden file inputs. Must be rendered in your component for the picker to work',
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, {useState} from 'react'
|
|
2
|
+
|
|
3
|
+
import {Camera, Image} from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import {Alert, AlertDescription, AlertTitle} from '../../components/ui/alert'
|
|
6
|
+
import {Button} from '../../components/ui/button'
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from '../../components/ui/card'
|
|
13
|
+
|
|
14
|
+
import {useImagePicker} from './useImagePicker'
|
|
15
|
+
|
|
16
|
+
export const UseImagePickerExample = () => {
|
|
17
|
+
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
|
18
|
+
const [error, setError] = useState<string | null>(null)
|
|
19
|
+
const {openCamera, openGallery, ImagePickerInputs} = useImagePicker({
|
|
20
|
+
cameraFacing: 'user',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const handleCameraCapture = async () => {
|
|
24
|
+
try {
|
|
25
|
+
setError(null)
|
|
26
|
+
const file = await openCamera()
|
|
27
|
+
const url = URL.createObjectURL(file)
|
|
28
|
+
setSelectedImage(url)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setError(err instanceof Error ? err.message : 'Failed to capture image')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleGallerySelect = async () => {
|
|
35
|
+
try {
|
|
36
|
+
setError(null)
|
|
37
|
+
const file = await openGallery()
|
|
38
|
+
const url = URL.createObjectURL(file)
|
|
39
|
+
setSelectedImage(url)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError(err instanceof Error ? err.message : 'Failed to select image')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader>
|
|
48
|
+
<CardTitle>Image Picker Example</CardTitle>
|
|
49
|
+
</CardHeader>
|
|
50
|
+
<CardContent className="space-y-4">
|
|
51
|
+
<ImagePickerInputs />
|
|
52
|
+
|
|
53
|
+
<div className="flex gap-2">
|
|
54
|
+
<Button onClick={handleCameraCapture} variant="primary">
|
|
55
|
+
<Camera className="mr-2 h-4 w-4" />
|
|
56
|
+
Open Camera
|
|
57
|
+
</Button>
|
|
58
|
+
|
|
59
|
+
<Button onClick={handleGallerySelect} variant="secondary">
|
|
60
|
+
<Image className="mr-2 h-4 w-4" />
|
|
61
|
+
Open Gallery
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{error && (
|
|
66
|
+
<Alert variant="destructive">
|
|
67
|
+
<AlertTitle>Error</AlertTitle>
|
|
68
|
+
<AlertDescription>{error}</AlertDescription>
|
|
69
|
+
</Alert>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{selectedImage && (
|
|
73
|
+
<div className="space-y-2">
|
|
74
|
+
<h3 className="text-sm font-medium">Selected Image:</h3>
|
|
75
|
+
<img
|
|
76
|
+
src={selectedImage}
|
|
77
|
+
alt="Selected"
|
|
78
|
+
className="w-full rounded-md border"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CameraFacing,
|
|
3
|
+
useImagePickerContext,
|
|
4
|
+
} from '../../providers/ImagePickerProvider'
|
|
5
|
+
|
|
6
|
+
interface UseImagePickerReturn {
|
|
7
|
+
openCamera: (cameraFacing?: CameraFacing) => Promise<File>
|
|
8
|
+
openGallery: () => Promise<File>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useImagePicker(): UseImagePickerReturn {
|
|
12
|
+
const {openCamera, openGallery} = useImagePickerContext()
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
openCamera,
|
|
16
|
+
openGallery,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
} from 'react'
|
|
9
|
+
|
|
10
|
+
export type CameraFacing = 'front' | 'back'
|
|
11
|
+
|
|
12
|
+
interface ImagePickerContextValue {
|
|
13
|
+
openCamera: (cameraFacing?: CameraFacing) => Promise<File>
|
|
14
|
+
openGallery: () => Promise<File>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ImagePickerContext = createContext<ImagePickerContextValue | null>(null)
|
|
18
|
+
|
|
19
|
+
export function useImagePickerContext() {
|
|
20
|
+
const context = useContext(ImagePickerContext)
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'useImagePickerContext must be used within an ImagePickerProvider'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return context
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ImagePickerProviderProps {
|
|
30
|
+
children: React.ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ImagePickerProvider({children}: ImagePickerProviderProps) {
|
|
34
|
+
const galleryInputRef = useRef<HTMLInputElement>(null)
|
|
35
|
+
const frontCameraInputRef = useRef<HTMLInputElement>(null)
|
|
36
|
+
const backCameraInputRef = useRef<HTMLInputElement>(null)
|
|
37
|
+
const resolveRef = useRef<((file: File) => void) | null>(null)
|
|
38
|
+
const rejectRef = useRef<((reason: Error) => void) | null>(null)
|
|
39
|
+
const activeCancelHandlerRef = useRef<{
|
|
40
|
+
input: HTMLInputElement
|
|
41
|
+
handler: () => void
|
|
42
|
+
} | null>(null)
|
|
43
|
+
|
|
44
|
+
const cleanupCancelHandler = useCallback(() => {
|
|
45
|
+
if (activeCancelHandlerRef.current) {
|
|
46
|
+
const {input, handler} = activeCancelHandlerRef.current
|
|
47
|
+
input.removeEventListener('cancel', handler)
|
|
48
|
+
activeCancelHandlerRef.current = null
|
|
49
|
+
}
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const rejectPendingPromise = useCallback(() => {
|
|
53
|
+
if (rejectRef.current) {
|
|
54
|
+
rejectRef.current(
|
|
55
|
+
new Error('New file picker opened before previous completed')
|
|
56
|
+
)
|
|
57
|
+
resolveRef.current = null
|
|
58
|
+
rejectRef.current = null
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
const handleFileChange = useCallback(
|
|
63
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
64
|
+
const file = event.target.files?.[0]
|
|
65
|
+
|
|
66
|
+
if (file && resolveRef.current) {
|
|
67
|
+
resolveRef.current(file)
|
|
68
|
+
|
|
69
|
+
resolveRef.current = null
|
|
70
|
+
rejectRef.current = null
|
|
71
|
+
|
|
72
|
+
cleanupCancelHandler()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
event.target.value = ''
|
|
76
|
+
},
|
|
77
|
+
[cleanupCancelHandler]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const openGallery = useCallback(() => {
|
|
81
|
+
return new Promise<File>((resolve, reject) => {
|
|
82
|
+
rejectPendingPromise()
|
|
83
|
+
cleanupCancelHandler()
|
|
84
|
+
|
|
85
|
+
resolveRef.current = resolve
|
|
86
|
+
rejectRef.current = reject
|
|
87
|
+
|
|
88
|
+
const input = galleryInputRef.current
|
|
89
|
+
|
|
90
|
+
if (!input) {
|
|
91
|
+
reject(new Error('Gallery input not found'))
|
|
92
|
+
resolveRef.current = null
|
|
93
|
+
rejectRef.current = null
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const handleCancel = () => {
|
|
98
|
+
if (rejectRef.current) {
|
|
99
|
+
rejectRef.current(new Error('User cancelled file selection'))
|
|
100
|
+
resolveRef.current = null
|
|
101
|
+
rejectRef.current = null
|
|
102
|
+
}
|
|
103
|
+
cleanupCancelHandler()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
input.addEventListener('cancel', handleCancel)
|
|
107
|
+
activeCancelHandlerRef.current = {input, handler: handleCancel}
|
|
108
|
+
|
|
109
|
+
input.click()
|
|
110
|
+
})
|
|
111
|
+
}, [rejectPendingPromise, cleanupCancelHandler])
|
|
112
|
+
|
|
113
|
+
const openCamera = useCallback(
|
|
114
|
+
(cameraFacing: CameraFacing = 'back') => {
|
|
115
|
+
return new Promise<File>((resolve, reject) => {
|
|
116
|
+
rejectPendingPromise()
|
|
117
|
+
cleanupCancelHandler()
|
|
118
|
+
|
|
119
|
+
resolveRef.current = resolve
|
|
120
|
+
rejectRef.current = reject
|
|
121
|
+
|
|
122
|
+
const input =
|
|
123
|
+
cameraFacing === 'front'
|
|
124
|
+
? frontCameraInputRef.current
|
|
125
|
+
: backCameraInputRef.current
|
|
126
|
+
|
|
127
|
+
if (!input) {
|
|
128
|
+
reject(new Error('Camera input not found'))
|
|
129
|
+
resolveRef.current = null
|
|
130
|
+
rejectRef.current = null
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const handleCancel = () => {
|
|
135
|
+
if (rejectRef.current) {
|
|
136
|
+
rejectRef.current(new Error('User cancelled camera'))
|
|
137
|
+
resolveRef.current = null
|
|
138
|
+
rejectRef.current = null
|
|
139
|
+
}
|
|
140
|
+
cleanupCancelHandler()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
input.addEventListener('cancel', handleCancel)
|
|
144
|
+
activeCancelHandlerRef.current = {input, handler: handleCancel}
|
|
145
|
+
|
|
146
|
+
input.click()
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
[rejectPendingPromise, cleanupCancelHandler]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
return () => {
|
|
154
|
+
rejectPendingPromise()
|
|
155
|
+
cleanupCancelHandler()
|
|
156
|
+
}
|
|
157
|
+
}, [rejectPendingPromise, cleanupCancelHandler])
|
|
158
|
+
|
|
159
|
+
const contextValue: ImagePickerContextValue = useMemo(
|
|
160
|
+
() => ({
|
|
161
|
+
openCamera,
|
|
162
|
+
openGallery,
|
|
163
|
+
}),
|
|
164
|
+
[openCamera, openGallery]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<ImagePickerContext.Provider value={contextValue}>
|
|
169
|
+
{children}
|
|
170
|
+
<input
|
|
171
|
+
ref={galleryInputRef}
|
|
172
|
+
type="file"
|
|
173
|
+
accept="image/*"
|
|
174
|
+
onChange={handleFileChange}
|
|
175
|
+
style={{display: 'none'}}
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
/>
|
|
178
|
+
<input
|
|
179
|
+
ref={frontCameraInputRef}
|
|
180
|
+
type="file"
|
|
181
|
+
accept="image/*"
|
|
182
|
+
capture="user"
|
|
183
|
+
onChange={handleFileChange}
|
|
184
|
+
style={{display: 'none'}}
|
|
185
|
+
aria-hidden="true"
|
|
186
|
+
/>
|
|
187
|
+
<input
|
|
188
|
+
ref={backCameraInputRef}
|
|
189
|
+
type="file"
|
|
190
|
+
accept="image/*"
|
|
191
|
+
capture="environment"
|
|
192
|
+
onChange={handleFileChange}
|
|
193
|
+
style={{display: 'none'}}
|
|
194
|
+
aria-hidden="true"
|
|
195
|
+
/>
|
|
196
|
+
</ImagePickerContext.Provider>
|
|
197
|
+
)
|
|
198
|
+
}
|