UNPKG

@shopify/shop-minis-react

Version:

React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)

168 lines (140 loc) 4.02 kB
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 } interface UseImageUploadReturns { /** * Upload an image which will be attached to the current user. */ uploadImage: (image: File) => Promise<UploadedImage[]> } // Fetch file data and detect file sizes if not provided // Works with file://, data:, and http(s):// URIs const processFileData = async (image: File): Promise<ProcessedImage> => { 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, } }