diff --git a/app/models/QRCode.server.js b/app/models/QRCode.server.js
index 6fe3abd..7d4bcc6 100644
--- a/app/models/QRCode.server.js
+++ b/app/models/QRCode.server.js
@@ -1,55 +1,71 @@
import qrcode from "qrcode";
import invariant from "tiny-invariant";
-import db from "../db.server";
+import {
+ getQRCodeById,
+ getQRCodes as getQRCodesFromRepo,
+} from "./qrcode.repository";
// [START get-qrcode]
-export async function getQRCode(id, graphql) {
- const qrCode = await db.qRCode.findFirst({ where: { id } });
+export async function getQRCode(id, graphql, shop) {
+ const qrCode = await getQRCodeById(graphql, id);
if (!qrCode) {
return null;
}
- return supplementQRCode(qrCode, graphql);
+ return supplementQRCode(qrCode, graphql, shop);
}
export async function getQRCodes(shop, graphql) {
- const qrCodes = await db.qRCode.findMany({
- where: { shop },
- orderBy: { id: "desc" },
- });
+ const qrCodes = await getQRCodesFromRepo(graphql);
if (qrCodes.length === 0) return [];
return Promise.all(
- qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql))
+ qrCodes.map((qrCode) => supplementQRCode(qrCode, graphql, shop))
);
}
// [END get-qrcode]
// [START get-qrcode-image]
-export function getQRCodeImage(id) {
- const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
+export function getQRCodeImage(id, shop) {
+ // Extract the numeric ID from the GID if needed
+ const idForUrl = extractIdFromGid(id);
+ const url = new URL(`/qrcodes/${idForUrl}/scan`, process.env.SHOPIFY_APP_URL);
+ // Include shop as query parameter for unauthenticated access
+ if (shop) {
+ url.searchParams.set("shop", shop);
+ }
return qrcode.toDataURL(url.href);
}
+
+function extractIdFromGid(gid) {
+ // If it's already a simple ID, return as-is
+ if (!gid.startsWith("gid://")) {
+ return gid;
+ }
+ // Extract the ID from gid://shopify/Metaobject/123456
+ const match = gid.match(/\/(\d+)$/);
+ return match ? match[1] : gid;
+}
// [END get-qrcode-image]
// [START get-destination]
-export function getDestinationUrl(qrCode) {
+export function getDestinationUrl(qrCode, shop) {
if (qrCode.destination === "product") {
- return `https://${qrCode.shop}/products/${qrCode.productHandle}`;
+ return `https://${shop}/products/${qrCode.productHandle}`;
}
const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/.exec(qrCode.productVariantId);
invariant(match, "Unrecognized product variant ID");
- return `https://${qrCode.shop}/cart/${match[1]}:1`;
+ return `https://${shop}/cart/${match[1]}:1`;
}
// [END get-destination]
// [START hydrate-qrcode]
-async function supplementQRCode(qrCode, graphql) {
- const qrCodeImagePromise = getQRCodeImage(qrCode.id);
+async function supplementQRCode(qrCode, graphql, shop) {
+ const qrCodeImagePromise = getQRCodeImage(qrCode.id, shop);
const response = await graphql(
`
@@ -82,11 +98,12 @@ async function supplementQRCode(qrCode, graphql) {
return {
...qrCode,
+ shop,
productDeleted: !product?.title,
productTitle: product?.title,
productImage: product?.media?.nodes[0]?.preview?.image?.url,
productAlt: product?.media?.nodes[0]?.preview?.image?.altText,
- destinationUrl: getDestinationUrl(qrCode),
+ destinationUrl: getDestinationUrl(qrCode, shop),
image: await qrCodeImagePromise,
};
}
diff --git a/app/models/qrcode.repository.js b/app/models/qrcode.repository.js
new file mode 100644
index 0000000..335eec0
--- /dev/null
+++ b/app/models/qrcode.repository.js
@@ -0,0 +1,271 @@
+/**
+ * QR Code Repository
+ * Handles all metaobject CRUD operations for QR codes
+ */
+
+const METAOBJECT_TYPE = "$app:qrcode";
+
+/**
+ * Get a single QR code by ID
+ */
+export async function getQRCodeById(graphql, id) {
+ const response = await graphql(
+ `#graphql
+ query GetQRCode($id: ID!) {
+ metaobject(id: $id) {
+ id
+ handle
+ updatedAt
+ title: field(key: "title") { value }
+ productId: field(key: "product_id") { value }
+ productHandle: field(key: "product_handle") { value }
+ productVariantId: field(key: "product_variant_id") { value }
+ destination: field(key: "destination") { value }
+ scans: field(key: "scans") { value }
+ }
+ }
+ `,
+ { variables: { id } }
+ );
+
+ const { data } = await response.json();
+
+ if (!data?.metaobject) {
+ return null;
+ }
+
+ return mapMetaobjectToQRCode(data.metaobject);
+}
+
+/**
+ * Get all QR codes for the shop
+ */
+export async function getQRCodes(graphql) {
+ const response = await graphql(
+ `#graphql
+ query GetQRCodes($type: String!) {
+ metaobjects(type: $type, first: 100, reverse: true) {
+ nodes {
+ id
+ handle
+ updatedAt
+ title: field(key: "title") { value }
+ productId: field(key: "product_id") { value }
+ productHandle: field(key: "product_handle") { value }
+ productVariantId: field(key: "product_variant_id") { value }
+ destination: field(key: "destination") { value }
+ scans: field(key: "scans") { value }
+ }
+ }
+ }
+ `,
+ { variables: { type: METAOBJECT_TYPE } }
+ );
+
+ const { data } = await response.json();
+
+ if (!data?.metaobjects?.nodes) {
+ return [];
+ }
+
+ return data.metaobjects.nodes.map(mapMetaobjectToQRCode);
+}
+
+/**
+ * Create a new QR code
+ */
+export async function createQRCode(graphql, qrCodeData) {
+ const response = await graphql(
+ `#graphql
+ mutation CreateQRCode($metaobject: MetaobjectCreateInput!) {
+ metaobjectCreate(metaobject: $metaobject) {
+ metaobject {
+ id
+ handle
+ title: field(key: "title") { value }
+ productId: field(key: "product_id") { value }
+ productHandle: field(key: "product_handle") { value }
+ productVariantId: field(key: "product_variant_id") { value }
+ destination: field(key: "destination") { value }
+ scans: field(key: "scans") { value }
+ }
+ userErrors {
+ field
+ message
+ code
+ }
+ }
+ }
+ `,
+ {
+ variables: {
+ metaobject: {
+ type: METAOBJECT_TYPE,
+ fields: [
+ { key: "title", value: qrCodeData.title },
+ { key: "product_id", value: qrCodeData.productId },
+ { key: "product_handle", value: qrCodeData.productHandle },
+ { key: "product_variant_id", value: qrCodeData.productVariantId },
+ { key: "destination", value: qrCodeData.destination },
+ { key: "scans", value: "0" },
+ ],
+ },
+ },
+ }
+ );
+
+ const { data } = await response.json();
+
+ if (data?.metaobjectCreate?.userErrors?.length > 0) {
+ throw new Error(data.metaobjectCreate.userErrors.map(e => e.message).join(", "));
+ }
+
+ return mapMetaobjectToQRCode(data.metaobjectCreate.metaobject);
+}
+
+/**
+ * Update an existing QR code
+ */
+export async function updateQRCode(graphql, id, qrCodeData) {
+ const response = await graphql(
+ `#graphql
+ mutation UpdateQRCode($id: ID!, $metaobject: MetaobjectUpdateInput!) {
+ metaobjectUpdate(id: $id, metaobject: $metaobject) {
+ metaobject {
+ id
+ handle
+ title: field(key: "title") { value }
+ productId: field(key: "product_id") { value }
+ productHandle: field(key: "product_handle") { value }
+ productVariantId: field(key: "product_variant_id") { value }
+ destination: field(key: "destination") { value }
+ scans: field(key: "scans") { value }
+ }
+ userErrors {
+ field
+ message
+ code
+ }
+ }
+ }
+ `,
+ {
+ variables: {
+ id,
+ metaobject: {
+ fields: [
+ { key: "title", value: qrCodeData.title },
+ { key: "product_id", value: qrCodeData.productId },
+ { key: "product_handle", value: qrCodeData.productHandle },
+ { key: "product_variant_id", value: qrCodeData.productVariantId },
+ { key: "destination", value: qrCodeData.destination },
+ ],
+ },
+ },
+ }
+ );
+
+ const { data } = await response.json();
+
+ if (data?.metaobjectUpdate?.userErrors?.length > 0) {
+ throw new Error(data.metaobjectUpdate.userErrors.map(e => e.message).join(", "));
+ }
+
+ return mapMetaobjectToQRCode(data.metaobjectUpdate.metaobject);
+}
+
+/**
+ * Delete a QR code
+ */
+export async function deleteQRCode(graphql, id) {
+ const response = await graphql(
+ `#graphql
+ mutation DeleteQRCode($id: ID!) {
+ metaobjectDelete(id: $id) {
+ deletedId
+ userErrors {
+ field
+ message
+ code
+ }
+ }
+ }
+ `,
+ { variables: { id } }
+ );
+
+ const { data } = await response.json();
+
+ if (data?.metaobjectDelete?.userErrors?.length > 0) {
+ throw new Error(data.metaobjectDelete.userErrors.map(e => e.message).join(", "));
+ }
+
+ return data.metaobjectDelete.deletedId;
+}
+
+/**
+ * Increment the scan count for a QR code
+ */
+export async function incrementQRCodeScans(graphql, id) {
+ // First get the current scan count
+ const qrCode = await getQRCodeById(graphql, id);
+
+ if (!qrCode) {
+ return null;
+ }
+
+ const newScans = (qrCode.scans || 0) + 1;
+
+ const response = await graphql(
+ `#graphql
+ mutation IncrementScans($id: ID!, $metaobject: MetaobjectUpdateInput!) {
+ metaobjectUpdate(id: $id, metaobject: $metaobject) {
+ metaobject {
+ id
+ scans: field(key: "scans") { value }
+ }
+ userErrors {
+ field
+ message
+ code
+ }
+ }
+ }
+ `,
+ {
+ variables: {
+ id,
+ metaobject: {
+ fields: [{ key: "scans", value: String(newScans) }],
+ },
+ },
+ }
+ );
+
+ const { data } = await response.json();
+
+ if (data?.metaobjectUpdate?.userErrors?.length > 0) {
+ throw new Error(data.metaobjectUpdate.userErrors.map(e => e.message).join(", "));
+ }
+
+ return { ...qrCode, scans: newScans };
+}
+
+/**
+ * Map a metaobject to a QR code object
+ */
+function mapMetaobjectToQRCode(metaobject) {
+ if (!metaobject) return null;
+
+ return {
+ id: metaobject.id,
+ handle: metaobject.handle,
+ createdAt: metaobject.updatedAt,
+ title: metaobject.title?.value || "",
+ productId: metaobject.productId?.value || "",
+ productHandle: metaobject.productHandle?.value || "",
+ productVariantId: metaobject.productVariantId?.value || "",
+ destination: metaobject.destination?.value || "",
+ scans: parseInt(metaobject.scans?.value || "0", 10),
+ };
+}
diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx
index d0001ea..7088719 100644
--- a/app/routes/app._index.jsx
+++ b/app/routes/app._index.jsx
@@ -16,6 +16,12 @@ import {
import { getQRCodes } from "../models/QRCode.server";
import { AlertDiamondIcon, ImageIcon } from "@shopify/polaris-icons";
+function extractIdFromGid(gid) {
+ if (!gid || typeof gid !== "string") return gid;
+ const match = gid.match(/Metaobject\/(\d+)/);
+ return match ? match[1] : gid;
+}
+
// [START loader]
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
@@ -66,7 +72,7 @@ const QRTable = ({ qrCodes }) => (
selectable={false}
>
{qrCodes.map((qrCode) => (
-
+
))}
);
@@ -74,7 +80,7 @@ const QRTable = ({ qrCodes }) => (
// [START row]
const QRTableRow = ({ qrCode }) => (
-
+