// Offer image upload: staged upload -> Shopify Files (write_files). Returns the CDN url once processed. import type { ActionFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; import { gql } from "../lib/shop.server"; const STAGE = `#graphql mutation BundleOffersStage($input: [StagedUploadInput!]!) { stagedUploadsCreate(input: $input) { stagedTargets { url resourceUrl parameters { name value } } userErrors { field message } } }`; const CREATE = `#graphql mutation BundleOffersFile($files: [FileCreateInput!]!) { fileCreate(files: $files) { files { id fileStatus ... on MediaImage { image { url } } } userErrors { field message } } }`; const STATUS = `#graphql query BundleOffersFileStatus($id: ID!) { node(id: $id) { ... on MediaImage { fileStatus image { url } } } }`; const MAX_BYTES = 10 * 1024 * 1024; export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); const form = await request.formData(); const file = form.get("file"); if (!(file instanceof File) || !file.size) return { error: "Choose an image." }; if (!file.type.startsWith("image/")) return { error: "Use a JPG, PNG, GIF or WebP image." }; if (file.size > MAX_BYTES) return { error: "Images must be 10 MB or smaller." }; const staged = await gql(admin, STAGE, { input: [{ filename: file.name, mimeType: file.type, resource: "IMAGE", httpMethod: "POST", fileSize: String(file.size) }], }); const target = staged.stagedUploadsCreate.stagedTargets?.[0]; if (!target) return { error: staged.stagedUploadsCreate.userErrors?.[0]?.message || "Upload failed." }; const body = new FormData(); for (const p of target.parameters) body.append(p.name, p.value); body.append("file", file); const up = await fetch(target.url, { method: "POST", body }); if (!up.ok) return { error: `Upload failed (${up.status}).` }; const created = await gql(admin, CREATE, { files: [{ originalSource: target.resourceUrl, contentType: "IMAGE", alt: "Bundle offer image" }] }); const f = created.fileCreate.files?.[0]; if (!f) return { error: created.fileCreate.userErrors?.[0]?.message || "Could not save the image." }; if (f.image?.url) return { url: f.image.url }; // processing is asynchronous: poll for a few seconds for (let i = 0; i < 10; i += 1) { await new Promise((r) => setTimeout(r, 1000)); const s = await gql(admin, STATUS, { id: f.id }); if (s.node?.image?.url) return { url: s.node.image.url as string }; if (s.node?.fileStatus === "FAILED") return { error: "Shopify could not process this image." }; } return { error: "The image is still processing. Try saving again in a minute." }; };