I deployed my remix app, and while submitting, it is failing at webhook HMAC verification step. After debugging found that app_uninstalled url is different than the actual deployed url, all other webhooks are correct but don’t know why this is of ngrok.
What am I doing wrong?
Here’s the code:
shopify.app.toml:
[webhooks]
api_version = "2024-07"
[[webhooks.subscriptions]]
uri = "/api/webhooks/customer_deletion"
compliance_topics = [ "customers/redact" ]
[[webhooks.subscriptions]]
uri = "/api/webhooks/customer_request"
compliance_topics = [ "customers/data_request" ]
[[webhooks.subscriptions]]
uri = "/api/webhooks/deletion"
compliance_topics = [ "shop/redact" ]
[[webhooks.subscriptions]]
uri = "/api/webhooks/app/uninstalled"
topics = [ "app/uninstalled" ]
webhooks.jsx:
// app/routes/webhooks.jsx
import { json } from "@remix-run/node";
import crypto from "crypto";
import { authenticate } from "../shopify.server";
import db from "../db.server";
export async function action({ request }) {
const { topic, shop, session, admin } = await authenticate.webhook(request);
if (request.method.toUpperCase() !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const bodyText = await request.text();
console.log("Raw bodyText:", bodyText);
// Verify the webhook is from Shopify
const hmacHeader = request.headers.get("X-Shopify-Hmac-Sha256");
console.log("Received HMAC Header:", hmacHeader);
const isValid = verifyShopifyWebhook(bodyText, hmacHeader);
console.log("isValid:", isValid);
if (!isValid) {
console.error("HMAC verification failed");
return new Response("Forbidden", { status: 403 });
} else {
return new Response("Success", { status: 200 });
}
// Parse the webhook payload
let data;
try {
data = JSON.parse(bodyText);
console.log("Webhook data:", data);
} catch (error) {
console.error("Error parsing JSON:", error);
return new Response("Bad Request", { status: 400 });
}
// Handle the event based on its type
switch (data.type) {
case "APP_UNINSTALLED":
if (session) {
console.log("Handling request for app uninstalled.");
await db.session.deleteMany({ where: { shop } });
}
break;
case "request_customer_data":
console.log("Handling request for customer data.");
// Handle customer data request
break;
case "delete_customer_data":
console.log("Handling deletion of customer data.");
// Handle customer data deletion
break;
case "delete_shop_data":
console.log("Handling deletion of shop data.");
// Handle shop data deletion
break;
default:
console.log("Unknown webhook type:", data.type);
return new Response("Webhook type not supported", { status: 400 });
}
// Respond to Shopify
return json({ success: true });
}
// Helper function to verify Shopify's HMAC signature
function verifyShopifyWebhook(data, hmacHeader) {
const secret = process.env.SHOPIFY_API_SECRET;
if (!secret) {
console.error("SHOPIFY_API_SECRET is not set");
return false;
}
const hash = crypto
.createHmac("sha256", secret)
.update(data, 'utf8') // Specify 'utf8' to ensure the correct encoding
.digest("base64");
console.log("Calculated HMAC Hash:", hash);
return hash === hmacHeader;
}
shopify.server.js:
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_DATA_REQUEST: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
SHOP_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},