diff --git a/app/routes/api.reviews.jsx b/app/routes/api.reviews.jsx index 63ff921..76e7e51 100644 --- a/app/routes/api.reviews.jsx +++ b/app/routes/api.reviews.jsx @@ -1,45 +1,178 @@ -import prisma from "../db.server"; +import { authenticate, unauthenticated } from "../shopify.server"; -// GET → fetch reviews -export const loader = async ({ request }) => { - const url = new URL(request.url); - const productId = url.searchParams.get("productId"); - - if (!productId) { - return new Response("Product ID missing", { status: 400 }); - } - - const reviews = await prisma.review.findMany({ - where: { productId }, - orderBy: { createdAt: "desc" }, - }); - - return new Response(JSON.stringify(reviews), { - headers: { "Content-Type": "application/json" }, +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", + }, }); }; -// POST → save review -export const action = async ({ request }) => { - const formData = await request.formData(); +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 name = formData.get("name"); - const review = formData.get("review"); - const productId = formData.get("productId"); + 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" } + ] + } + }; - if (!productId) { - return new Response("Product ID missing", { status: 400 }); + 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([]); } +}; - await prisma.review.create({ - data: { - productId, - name, - review, - }, - }); +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"); - return new Response(JSON.stringify({ success: true }), { - headers: { "Content-Type": "application/json" }, - }); + 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." }); + } }; \ No newline at end of file diff --git a/app/routes/webhooks.app.scopes_update.jsx b/app/routes/webhooks.app.scopes_update.jsx index a40946e..d7f21e6 100644 --- a/app/routes/webhooks.app.scopes_update.jsx +++ b/app/routes/webhooks.app.scopes_update.jsx @@ -1,5 +1,4 @@ import { authenticate } from "../shopify.server"; -import db from "../db.server"; export const action = async ({ request }) => { const { payload, session, topic, shop } = await authenticate.webhook(request); @@ -8,14 +7,7 @@ export const action = async ({ request }) => { const current = payload.current; if (session) { - await db.session.update({ - where: { - id: session.id, - }, - data: { - scope: current.toString(), - }, - }); + // In memory storage, we don't need to manually update the scope in a DB } return new Response(); diff --git a/app/routes/webhooks.app.uninstalled.jsx b/app/routes/webhooks.app.uninstalled.jsx index cdbada6..ea06f47 100644 --- a/app/routes/webhooks.app.uninstalled.jsx +++ b/app/routes/webhooks.app.uninstalled.jsx @@ -1,5 +1,4 @@ import { authenticate } from "../shopify.server"; -import db from "../db.server"; export const action = async ({ request }) => { const { shop, session, topic } = await authenticate.webhook(request); @@ -9,7 +8,8 @@ export const action = async ({ request }) => { // Webhook requests can trigger multiple times and after an app has already been uninstalled. // If this webhook already ran, the session may have been deleted previously. if (session) { - await db.session.deleteMany({ where: { shop } }); + // Session cleanup for memory storage is not strictly required here + // but you would normally call shopify.sessionStorage.deleteSessions([shop]) } return new Response(); diff --git a/app/shopify.server.js b/app/shopify.server.js index 691d476..d7e0a85 100644 --- a/app/shopify.server.js +++ b/app/shopify.server.js @@ -4,8 +4,7 @@ import { AppDistribution, shopifyApp, } from "@shopify/shopify-app-react-router/server"; -import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma"; -import prisma from "./db.server"; +import { MemorySessionStorage } from "@shopify/shopify-app-session-storage-memory"; const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY, @@ -14,7 +13,7 @@ const shopify = shopifyApp({ scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL || "", authPathPrefix: "/auth", - sessionStorage: new PrismaSessionStorage(prisma), + sessionStorage: new MemorySessionStorage(), distribution: AppDistribution.AppStore, future: { expiringOfflineAccessTokens: true,