import {useCallback} from 'react' import {useShopActions} from '../../internal/useShopActions' import {fileToDataUri} from '../../utils' import type {UploadTarget} from '@shopify/shop-minis-platform/actions' export interface UploadImageParams { /** * The file to upload. */ image: File } interface ProcessedImage { /** * The MIME type of the image. */ mimeType: string /** * The size of the image in bytes. */ fileSize: number /** * The file blob of the image. */ fileBlob: Blob } export interface UploadedImage { /** * The ID of the uploaded image. */ id: string /** * The URL of the uploaded image. */ imageUrl?: string /** * The resource URL of the uploaded image. */ resourceUrl?: string } export interface UseImageUploadReturns { /** * Upload an image which will be attached to the current user. */ uploadImage: (image: File) => Promise } // Fetch file data and detect file sizes if not provided // Works with file://, data:, and http(s):// URIs const processFileData = async (image: File): Promise => { const uri = await fileToDataUri(image) const response = await fetch(uri) const blob = await response.blob() return { mimeType: image.type, fileSize: image.size ?? blob.size, fileBlob: blob, } } const uploadFileToGCS = async (image: ProcessedImage, target: UploadTarget) => { const formData = new FormData() target.parameters.forEach(({name, value}: {name: string; value: string}) => { formData.append(name, value) }) formData.append('file', image.fileBlob) const uploadResponse = await fetch(target.url, { method: 'POST', body: formData, }) if (!uploadResponse.ok) { console.error('Failed to upload image', { response: await uploadResponse.text(), }) return {error: 'Failed to upload image'} } return {} } export const useImageUpload = (): UseImageUploadReturns => { const {createImageUploadLink, completeImageUpload} = useShopActions() const uploadImage = useCallback( async (image: File) => { const processedImageParams = await processFileData(image) const links = await createImageUploadLink({ input: [ { mimeType: processedImageParams.mimeType, fileSize: processedImageParams.fileSize, }, ], }) if (!links.ok) { throw new Error(links.error.message) } if (links.mocked) { // Skip upload and return mock data return [ { id: 'uploaded-image-id', imageUrl: 'https://cdn.shopify.com/s/files/1/0621/0463/3599/files/Mr._Bean_2007_800x800.jpg?v=1763126175', resourceUrl: 'https://cdn.shopify.com/s/files/1/0621/0463/3599/files/Mr._Bean_2007_800x800.jpg?v=1763126175', }, ] } // Upload single file to GCS const {error: uploadError} = await uploadFileToGCS( processedImageParams, links?.data?.targets?.[0]! ) if (uploadError) { throw new Error(uploadError) } // 10 second polling for image upload let count = 0 while (count < 30) { const result = await completeImageUpload({ resourceUrls: links?.data?.targets?.map(target => target.resourceUrl) || [], }) if (!result.ok) { throw new Error(result.error.message) } if (result.data?.files?.[0]?.fileStatus === 'READY') { return [ { id: result.data.files[0].id, imageUrl: result.data.files[0].image?.url, resourceUrl: links?.data?.targets?.[0]?.resourceUrl, }, ] } await new Promise(resolve => setTimeout(resolve, 1000)) count++ } throw new Error('Image upload completion timed out') }, [createImageUploadLink, completeImageUpload] ) return { uploadImage, } } /** * The `useImageUpload` hook provides image upload functionality, handling the complete upload pipeline. It returns basic image metadata (ID, URL, resource URL) with uploads automatically attached to the current user. This hook is the foundation for direct image storage scenarios like profile pictures, attachments, or cases where you manage image relationships and metadata yourself. For user-generated content requiring visibility controls, sharing capabilities, or content discovery features, `useCreateImageContent` builds on this hook to provide those higher-level abstractions. * @publicDocs */ export type UseImageUploadGeneratedType = () => UseImageUploadReturns