From 3bf02fbf87d9c23d4b4c657e7187b273eb2df355 Mon Sep 17 00:00:00 2001 From: Richard Powell Date: Tue, 10 Sep 2024 13:46:25 -0400 Subject: [PATCH] Make it easier to get started with webhooks by: 1. Providing the config for mandatory webhook topics 2. Adding a product create webhook, so partners can see a webhook trigger when pressing "generte a product" 3. Separating each webhook topic into it's own URL --- app/routes/webhooks.app.uninstalled.tsx | 15 +++++++++ .../webhooks.customers.data_request.tsx | 18 +++++++++++ app/routes/webhooks.customers.redact.tsx | 18 +++++++++++ app/routes/webhooks.products.create.tsx | 17 ++++++++++ app/routes/webhooks.shop.redact.tsx | 18 +++++++++++ app/routes/webhooks.tsx | 32 ------------------- shopify.app.toml | 24 +++++++++++++- shopify.web.toml | 2 +- 8 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 app/routes/webhooks.app.uninstalled.tsx create mode 100644 app/routes/webhooks.customers.data_request.tsx create mode 100644 app/routes/webhooks.customers.redact.tsx create mode 100644 app/routes/webhooks.products.create.tsx create mode 100644 app/routes/webhooks.shop.redact.tsx delete mode 100644 app/routes/webhooks.tsx diff --git a/app/routes/webhooks.app.uninstalled.tsx b/app/routes/webhooks.app.uninstalled.tsx new file mode 100644 index 00000000..f06e4872 --- /dev/null +++ b/app/routes/webhooks.app.uninstalled.tsx @@ -0,0 +1,15 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { authenticate } from "~/shopify.server"; +import db from "~/db.server"; + +export const action = async ({ request }: ActionFunctionArgs) => { + const { shop, session } = await authenticate.webhook(request); + + // Webhook requests can trigger after an app is uninstalled. + // If the app is already uninstalled, the session may be undefined. + if (session) { + db.session.deleteMany({ where: { shop } }); + } + + return new Response(); +}; \ No newline at end of file diff --git a/app/routes/webhooks.customers.data_request.tsx b/app/routes/webhooks.customers.data_request.tsx new file mode 100644 index 00000000..55357812 --- /dev/null +++ b/app/routes/webhooks.customers.data_request.tsx @@ -0,0 +1,18 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { authenticate } from "~/shopify.server"; + +export const action = async ({ request }: ActionFunctionArgs) => { + const { session } = await authenticate.webhook(request); + + // Webhook requests can trigger after an app is uninstalled + // If the app is already uninstalled, the session may be undefined. + if (!session) { + throw new Response(); + } + + // Implement handling of mandatory compliance topics + // See: https://shopify.dev/docs/apps/build/privacy-law-compliance + console.log("Received customers data_request webhook"); + + return new Response(); +}; diff --git a/app/routes/webhooks.customers.redact.tsx b/app/routes/webhooks.customers.redact.tsx new file mode 100644 index 00000000..522e566f --- /dev/null +++ b/app/routes/webhooks.customers.redact.tsx @@ -0,0 +1,18 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { authenticate } from "~/shopify.server"; + +export const action = async ({ request }: ActionFunctionArgs) => { + const {session} = await authenticate.webhook(request); + + // Webhook requests can trigger after an app is uninstalled + // If the app is already uninstalled, the session may be undefined. + if (!session) { + throw new Response(); + } + + // Implement handling of mandatory compliance topics + // See: https://shopify.dev/docs/apps/build/privacy-law-compliance + console.log("Received customers redact webhook"); + + return new Response(); +}; diff --git a/app/routes/webhooks.products.create.tsx b/app/routes/webhooks.products.create.tsx new file mode 100644 index 00000000..72c952f5 --- /dev/null +++ b/app/routes/webhooks.products.create.tsx @@ -0,0 +1,17 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { authenticate } from "~/shopify.server"; + +export const action = async ({ request }: ActionFunctionArgs) => { + const { session, payload } = await authenticate.webhook(request); + + // Webhook requests can trigger after an app is uninstalled + // If the app is already uninstalled, the session may be undefined. + if (!session) { + return new Response() + } + + console.log("Received products create webhook"); + console.log(JSON.stringify(payload)); + + return new Response(); +}; \ No newline at end of file diff --git a/app/routes/webhooks.shop.redact.tsx b/app/routes/webhooks.shop.redact.tsx new file mode 100644 index 00000000..d08241da --- /dev/null +++ b/app/routes/webhooks.shop.redact.tsx @@ -0,0 +1,18 @@ +import type { ActionFunctionArgs } from "@remix-run/node"; +import { authenticate } from "~/shopify.server"; + +export const action = async ({ request }: ActionFunctionArgs) => { + const { session } = await authenticate.webhook(request); + + // SHOP_REDACT will be fired up to 48 hours after app is uninstalled + // Therefore, for SHOP_REDACT we expect the admin to be undefined + if (!session) { + return new Response("", { status: 400 }); + } + + // Implement handling of mandatory compliance topics + // See: https://shopify.dev/docs/apps/build/privacy-law-compliance + console.log("Received shop redact webhook"); + + return new Response(); +}; diff --git a/app/routes/webhooks.tsx b/app/routes/webhooks.tsx deleted file mode 100644 index c19f57ac..00000000 --- a/app/routes/webhooks.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { ActionFunctionArgs } from "@remix-run/node"; -import { authenticate } from "../shopify.server"; -import db from "../db.server"; - -export const action = async ({ request }: ActionFunctionArgs) => { - const { topic, shop, session, admin } = await authenticate.webhook(request); - - if (!admin && topic !== 'SHOP_REDACT') { - // The admin context isn't returned if the webhook fired after a shop was uninstalled. - // The SHOP_REDACT webhook will be fired up to 48 hours after a shop uninstalls the app. - // Because of this, no admin context is available. - throw new Response(); - } - - // The topics handled here should be declared in the shopify.app.toml. - // More info: https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration - switch (topic) { - case "APP_UNINSTALLED": - if (session) { - await db.session.deleteMany({ where: { shop } }); - } - - break; - case "CUSTOMERS_DATA_REQUEST": - case "CUSTOMERS_REDACT": - case "SHOP_REDACT": - default: - throw new Response("Unhandled webhook topic", { status: 404 }); - } - - throw new Response(); -}; diff --git a/shopify.app.toml b/shopify.app.toml index d7dfe14b..fc76a182 100644 --- a/shopify.app.toml +++ b/shopify.app.toml @@ -5,6 +5,28 @@ scopes = "write_products" [webhooks] api_version = "2024-07" + # Mandatory compliance topics for public apps + # Delete only if the app is not public + # See: https://shopify.dev/docs/apps/build/privacy-law-compliance [[webhooks.subscriptions]] + uri = "/webhooks/customers/data_request" + compliance_topics = [ "customers/data_request"] + + [[webhooks.subscriptions]] + uri = "/webhooks/customers/redact" + compliance_topics = [ "customers/redact" ] + + [[webhooks.subscriptions]] + uri = "/webhooks/shop/redact" + compliance_topics = [ "shop/redact" ] + + # When a shop uninstalls the app, delete the session for that shop + [[webhooks.subscriptions]] + uri = "/webhooks/app/uninstalled" topics = [ "app/uninstalled" ] - uri = "/webhooks" + + # Optional: Configured for illustration purposes + # Delete if the app doesn't care about the product create event + [[webhooks.subscriptions]] + topics = ["products/create"] + uri = "/webhooks/products/create" diff --git a/shopify.web.toml b/shopify.web.toml index eb29b5b5..b35c57be 100644 --- a/shopify.web.toml +++ b/shopify.web.toml @@ -1,6 +1,6 @@ name = "remix" roles = ["frontend", "backend"] -webhooks_path = "/webhooks" +webhooks_path = "/webhooks/app/uninstalled" [commands] predev = "npx prisma generate"