import React, { createContext, useContext, useRef, useCallback, useEffect, useMemo, } from 'react' import {useRequestPermissions} from '../hooks/util/useRequestPermissions' import {useReportInteraction} from '../internal/useReportInteraction' import {resizeImage} from '../internal/utils/resizeImage' /** @publicDocs */ export type ImageQuality = 'low' | 'medium' | 'high' | 'original' /** @publicDocs */ export type CameraFacing = 'front' | 'back' /** @publicDocs */ export interface CustomImageQuality { size?: number compression?: number } /** @publicDocs */ export interface OpenCameraParams { cameraFacing?: CameraFacing quality?: ImageQuality customQuality?: CustomImageQuality } /** @publicDocs */ export interface OpenGalleryParams { quality?: ImageQuality customQuality?: CustomImageQuality } interface ImagePickerContextValue { openCamera: (params?: OpenCameraParams) => Promise openGallery: (params?: OpenGalleryParams) => Promise } const ImagePickerContext = createContext(null) export function useImagePickerContext() { const context = useContext(ImagePickerContext) if (!context) { throw new Error( 'useImagePickerContext must be used within an ImagePickerProvider' ) } return context } interface ImagePickerProviderProps { children: React.ReactNode } export function ImagePickerProvider({children}: ImagePickerProviderProps) { const galleryInputRef = useRef(null) const frontCameraInputRef = useRef(null) const backCameraInputRef = useRef(null) const resolveRef = useRef<((file: File) => void) | null>(null) const rejectRef = useRef<((reason: Error) => void) | null>(null) const activeCancelHandlerRef = useRef<{ input: HTMLInputElement handler: () => void } | null>(null) const activeOperationRef = useRef<'gallery' | 'camera' | null>(null) const qualityRef = useRef('medium') const customQualityRef = useRef(undefined) const {requestPermission} = useRequestPermissions() const {reportInteraction} = useReportInteraction() const cleanupCancelHandler = useCallback(() => { if (activeCancelHandlerRef.current) { const {input, handler} = activeCancelHandlerRef.current input.removeEventListener('cancel', handler) activeCancelHandlerRef.current = null } }, []) const rejectPendingPromise = useCallback(() => { if (rejectRef.current) { const error = new Error( 'New file picker opened before previous completed' ) if (activeOperationRef.current === 'gallery') { reportInteraction({ interactionType: 'image_picker_error', interactionValue: error.message, }) } else if (activeOperationRef.current === 'camera') { reportInteraction({ interactionType: 'camera_error', interactionValue: error.message, }) } rejectRef.current(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null qualityRef.current = 'medium' customQualityRef.current = undefined } }, [reportInteraction]) const handleFileChange = useCallback( async (event: React.ChangeEvent) => { const {target} = event const file = target.files?.[0] if (file && resolveRef.current) { try { const resizedFile = await resizeImage({ file, quality: qualityRef.current, customQuality: customQualityRef.current, }) if (activeOperationRef.current === 'gallery') { reportInteraction({ interactionType: 'image_picker_success', }) } else if (activeOperationRef.current === 'camera') { reportInteraction({ interactionType: 'camera_success', }) } resolveRef.current(resizedFile) } catch (error) { console.warn('Image resize failed, using original:', error) if (resolveRef.current) { resolveRef.current(file) } } resolveRef.current = null rejectRef.current = null activeOperationRef.current = null cleanupCancelHandler() } target.value = '' }, [cleanupCancelHandler, reportInteraction] ) const openGallery = useCallback( ({quality = 'medium', customQuality}: OpenGalleryParams = {}) => { return new Promise((resolve, reject) => { rejectPendingPromise() cleanupCancelHandler() qualityRef.current = quality customQualityRef.current = customQuality resolveRef.current = resolve rejectRef.current = reject activeOperationRef.current = 'gallery' const input = galleryInputRef.current if (!input) { const error = new Error('Gallery input not found') reportInteraction({ interactionType: 'image_picker_error', interactionValue: error.message, }) reject(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null return } const handleCancel = () => { if (rejectRef.current) { const error = new Error('User cancelled file selection') reportInteraction({ interactionType: 'image_picker_error', interactionValue: error.message, }) rejectRef.current(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null } cleanupCancelHandler() } input.addEventListener('cancel', handleCancel) activeCancelHandlerRef.current = {input, handler: handleCancel} reportInteraction({ interactionType: 'image_picker_open', }) requestPermission({permission: 'CAMERA'}) .then(() => { // This will show both Camera and Gallery input.click() }) .catch(() => { // Show only Gallery input.click() }) }) }, [ rejectPendingPromise, cleanupCancelHandler, requestPermission, reportInteraction, ] ) const openCamera = useCallback( ({ cameraFacing = 'back', quality = 'medium', customQuality, }: OpenCameraParams = {}) => { return new Promise((resolve, reject) => { rejectPendingPromise() cleanupCancelHandler() qualityRef.current = quality customQualityRef.current = customQuality resolveRef.current = resolve rejectRef.current = reject activeOperationRef.current = 'camera' const input = cameraFacing === 'front' ? frontCameraInputRef.current : backCameraInputRef.current if (!input) { const error = new Error('Camera input not found') reportInteraction({ interactionType: 'camera_error', interactionValue: error.message, }) reject(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null return } const handleCancel = () => { if (rejectRef.current) { const error = new Error('User cancelled camera') reportInteraction({ interactionType: 'camera_error', interactionValue: error.message, }) rejectRef.current(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null } cleanupCancelHandler() } input.addEventListener('cancel', handleCancel) activeCancelHandlerRef.current = {input, handler: handleCancel} reportInteraction({ interactionType: 'camera_open', }) requestPermission({permission: 'CAMERA'}) .then(({granted}) => { if (granted) { input.click() } else { const error = new Error('Camera permission not granted') reportInteraction({ interactionType: 'camera_error', interactionValue: error.message, }) reject(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null } }) .catch(() => { const error = new Error('Camera permission not granted') reject(error) resolveRef.current = null rejectRef.current = null activeOperationRef.current = null }) }) }, [ rejectPendingPromise, cleanupCancelHandler, requestPermission, reportInteraction, ] ) useEffect(() => { return () => { rejectPendingPromise() cleanupCancelHandler() } }, [rejectPendingPromise, cleanupCancelHandler]) const contextValue: ImagePickerContextValue = useMemo( () => ({ openCamera, openGallery, }), [openCamera, openGallery] ) return ( {children} ) }