Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions app/models/QRCode.server.js
Original file line number Diff line number Diff line change
@@ -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(
`
Expand Down Expand Up @@ -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,
};
}
Expand Down
271 changes: 271 additions & 0 deletions app/models/qrcode.repository.js
Original file line number Diff line number Diff line change
@@ -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),
};
}
Loading