Skip to content

Commit

Permalink
Add basic support for webhook registration and handling
Browse files Browse the repository at this point in the history
Add basic support for registration and handling of Shopify webhook
notifications, as a starter example.

Key pieces:

- `registerWebhook` function in *lib/shopify.js* registers a webhook
  via the admin API; creates a new registration or updates an
  existing one as appropriate
- add endpoint */api/shopify/webhook/TOPIC/* that receives, verifies,
  and prints to console the content of webhook notifications
- register the `app/uninstalled` webhook after initial auth, like
  the original example Shopify project

Also includes an endpoint */api/shopify/register-webhooks* you can
hit manually from within the Shopify Admin [1] to register webhooks,
as a convenience to add or update webhooks since the after-auth hook
is rarely triggered (unlike in the original Shopify example)

[1]: https://MYSTORE.myshopify.com/admin/apps/MYAPP/api/shopify/register-webhooks
  • Loading branch information
jmurty committed Apr 1, 2021
1 parent 8692943 commit bb3525e
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
73 changes: 73 additions & 0 deletions lib/shopify.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,84 @@ const shopifyAuthOptions = {
shopifyAssociatedUser
)}`
);

await registerWebhook(
shopOrigin,
shopifyToken,
"app/uninstalled",
"/api/shopify/webhook/app/uninstalled"
);

res.writeHead(302, { Location: `/` });
res.end();
},
};

const registerWebhook = async (
shopOrigin,
shopifyToken,
topic,
callbackUrlPathPrefix
) => {
const webhooksApiBaseUrl = `https://${shopOrigin}/admin/api/${ApiVersion.October20}/webhooks`;

const webhookCallbackUrl = `${process.env.SHOPIFY_APP_URL}${callbackUrlPathPrefix}/${topic}`;

const defaultRequestOptions = {
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": shopifyToken,
},
};

// Check whether webhook is already registered?
let url = new URL(`${webhooksApiBaseUrl}.json`);
url.searchParams.append("topic", topic);
url.searchParams.append("address", webhookCallbackUrl);
let response = await fetch(url, defaultRequestOptions);
const registeredWebhooks = await response.json();
const registeredWebhookId = registeredWebhooks["webhooks"][0]?.id;

// Update webhook with PUT if it is already registered...
if (registeredWebhookId) {
response = await fetch(
`${webhooksApiBaseUrl}/${registeredWebhookId}.json`,
{
...defaultRequestOptions,
method: "put",
body: JSON.stringify({
webhook: {
id: registeredWebhookId,
address: webhookCallbackUrl,
},
}),
}
);
}
// ...otherwise register new webhook
else {
response = await fetch(`${webhooksApiBaseUrl}.json`, {
...defaultRequestOptions,
method: "post",
body: JSON.stringify({
webhook: {
topic: topic,
format: "json",
address: webhookCallbackUrl,
},
}),
});
}

const result = await response.json();

console.log("Registered webhook", result["webhook"]);

return result;
};

const shopify = createShopifyAuth(shopifyAuthOptions);

export default shopify;

export { registerWebhook };
51 changes: 51 additions & 0 deletions pages/api/shopify/register-webhooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { authenticateShopifyAPI } from "@bluebeela/nextjs-shopify-auth";
import { registerWebhook } from "../../../lib/shopify";

export default authenticateShopifyAPI(async function handler(req, res) {
const { shopOrigin, shopifyToken } = req;
const callbackUrlPathPrefix = "/api/shopify/webhook";

await registerWebhook(
shopOrigin,
shopifyToken,
"app/uninstalled",
callbackUrlPathPrefix
);

await registerWebhook(
shopOrigin,
shopifyToken,
"carts/create",
callbackUrlPathPrefix
);

await registerWebhook(
shopOrigin,
shopifyToken,
"carts/update",
callbackUrlPathPrefix
);

await registerWebhook(
shopOrigin,
shopifyToken,
"checkouts/create",
callbackUrlPathPrefix
);

await registerWebhook(
shopOrigin,
shopifyToken,
"checkouts/update",
callbackUrlPathPrefix
);

await registerWebhook(
shopOrigin,
shopifyToken,
"orders/paid",
callbackUrlPathPrefix
);

res.status(200).end();
});
25 changes: 25 additions & 0 deletions pages/api/shopify/webhook/[...topic].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import crypto from "crypto";

const isVerifiedWebhookRequest = (req) => {
const expectedHash = req.headers["x-shopify-hmac-sha256"];
const actualHash = crypto
.createHmac("sha256", process.env.SHOPIFY_API_SECRET)
.update(JSON.stringify(req.body))
.digest("base64");

return expectedHash === actualHash;
};

export default async function webhooks(req, res) {
const { query } = req;
const topic = query.topic.join("/");

if (!isVerifiedWebhookRequest(req)) {
console.log(`Ignoring UNVERIFIED Shopify webhook ${topic}`);
res.status(400).end();
return;
}

console.log(`Received Shopify webhook ${topic}`, req.body);
res.status(200).end();
}

0 comments on commit bb3525e

Please sign in to comment.