import { authenticate, unauthenticated } from "../shopify.server"; const jsonResponse = (data, status = 200) => { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Cache-Control": "no-store", }, }); }; async function ensureDefinition(admin) { console.log(">>> [DEBUG] Triggering Metaobject Definition Creation..."); const mutation = `#graphql mutation CreateMetaobjectDefinition($definition: MetaobjectDefinitionCreateInput!) { metaobjectDefinitionCreate(definition: $definition) { metaobjectDefinition { type } userErrors { field message } } } `; const variables = { definition: { name: "Product Review", type: "product_review", access: { admin: "MERCHANT_READ_WRITE", storefront: "PUBLIC_READ" }, capabilities: { publishable: { enabled: true } }, fieldDefinitions: [ { name: "Product ID", key: "product_id", type: "single_line_text_field", required: true }, { name: "Customer Name", key: "customer_name", type: "single_line_text_field" }, { name: "Review Content", key: "content", type: "multi_line_text_field" }, { name: "Rating", key: "rating", type: "single_line_text_field" } ] } }; const response = await admin.graphql(mutation, { variables }); const result = await response.json(); console.log(">>> [DEBUG] Creation Result:", JSON.stringify(result)); return result; } export const loader = async ({ request }) => { try { const { admin, session } = await authenticate.public.appProxy(request); const url = new URL(request.url); const productId = url.searchParams.get("productId"); if (!productId) return jsonResponse([]); const query = `#graphql query getReviews($query: String!) { metaobjects(first: 50, type: "product_review", query: $query) { edges { node { fields { key value } } } } } `; const response = await admin.graphql(query, { variables: { query: `product_id:${productId}` }, }); const data = await response.json(); // Check for "No definition" error const isMissing = data.errors?.some(e => e.message.toLowerCase().includes("metaobject definition")) || !data.data?.metaobjects; if (isMissing) { console.log(">>> [DEBUG] Loader: Definition missing. Attempting to create..."); await ensureDefinition(admin); return jsonResponse([]); } const reviews = (data.data?.metaobjects?.edges || []).map((edge) => { const fields = {}; edge.node.fields.forEach((f) => { fields[f.key] = f.value; }); return { name: fields.customer_name || "Anonymous", review: fields.content || "", rating: fields.rating || "5", }; }); return jsonResponse(reviews); } catch (err) { console.error(">>> [ERROR] Loader:", err.message); return jsonResponse([]); } }; export const action = async ({ request }) => { try { console.log(">>> [DEBUG] Action: Review Submission Started"); const { admin, session } = await authenticate.public.appProxy(request); const url = new URL(request.url); const customerId = url.searchParams.get("logged_in_customer_id"); if (!customerId) return jsonResponse({ error: "Please log in to leave a review." }); const formData = await request.formData(); const productId = formData.get("productId"); const name = formData.get("name"); const review = formData.get("review"); // 1. Purchase Check const orderQuery = `#graphql query getOrders($query: String!) { orders(first: 10, query: $query) { edges { node { lineItems(first: 20) { edges { node { product { id } } } } } } } } `; const orderResponse = await admin.graphql(orderQuery, { variables: { query: `customer_id:${customerId}` } }); const orderData = await orderResponse.json(); const hasPurchased = (orderData.data?.orders?.edges || []).some(o => o.node.lineItems.edges.some(i => i.node.product?.id?.includes(productId)) ); if (!hasPurchased) { console.log(">>> [DEBUG] Action: No purchase history found for Customer ID:", customerId); return jsonResponse({ error: "Only verified buyers can leave a review. (Note: It may take a few minutes for new orders to sync)" }); } // 2. Save Review const saveMutation = `#graphql mutation create($m: MetaobjectCreateInput!) { metaobjectCreate(metaobject: $m) { metaobject { id } userErrors { field message } } } `; const saveResponse = await admin.graphql(saveMutation, { variables: { m: { type: "product_review", fields: [ { key: "product_id", value: String(productId) }, { key: "customer_name", value: String(name) }, { key: "content", value: String(review) }, { key: "rating", value: "5" } ] } } }); const result = await saveResponse.json(); console.log(">>> [DEBUG] Save Result:", JSON.stringify(result)); // Check for "No definition" error const isMissing = result.errors?.some(e => e.message.toLowerCase().includes("metaobject definition")) || result.data?.metaobjectCreate === null; if (isMissing) { console.log(">>> [DEBUG] Action: Definition missing. Creating structure..."); const createResult = await ensureDefinition(admin); const creationError = createResult.data?.metaobjectDefinitionCreate?.userErrors?.[0]?.message; if (creationError && !creationError.includes("already exists")) { return jsonResponse({ error: "Storefront permissions need update. Please open the App Admin page once to fix." }); } return jsonResponse({ error: "SUCCESS: Database structure created! Please click Submit Review one last time to save your review." }); } const errors = result.data?.metaobjectCreate?.userErrors; if (errors && errors.length > 0) { return jsonResponse({ error: errors[0].message }); } console.log(">>> [DEBUG] Action: Review Saved Successfully!"); return jsonResponse({ success: true }); } catch (err) { console.error(">>> [ERROR] Action Exception:", err.message); return jsonResponse({ error: "Connection issue. Please refresh the page and try again." }); } };