beef nachos
beef nachos

Reputation: 11

Handling External Webhook Payload on Node Shopify App

I am building a Node.js/React Shopify app that needs to update an order's delivery status based on payloads received from an external API's webhook. However, I am facing challenges in handling both webhook payloads and Shopify API mutations simultaneously.

Approach 1: Secure API Calls with validateAuthenticatedSession In this approach, I use Shopify's validateAuthenticatedSession middleware to ensure secure API calls. While this allows me to perform authenticated operations on orders (e.g., updating delivery status), it does not let me access payloads sent by the external API's webhook.

Approach 2: Direct Webhook Integration for External API Payloads In this approach, I create an endpoint without the validateAuthenticatedSession middleware. This allows me to successfully receive payloads from the external API's webhook. However, I cannot use Shopify API mutations in this setup because I lack a valid Shopify session, resulting in a 401 Unauthorized error.

Here is a gist with code snippets of the respective approaches. [https://gist.github.com/DevSheila/62b3970e4cf3c2c5a62d7cac36c204fa]

Question How can I design a solution that allows me to:

  1. Receive payloads from the external API's webhook.
  2. Use Shopify API mutations (e.g., update order status) securely and without errors.

I would appreciate guidance on reconciling these two approaches or any alternative suggestions.

index.js

// @ts-nocheck
import { join } from "path";
import { readFileSync } from "fs";
import express from "express";
import serveStatic from "serve-static";
import cors from "cors";
import shopify from "./shopify.js";

const PORT = parseInt(
  process.env.BACKEND_PORT || process.env.PORT || "3000",
  10
);

const STATIC_PATH =
  process.env.NODE_ENV === "production"
    ? `${process.cwd()}/frontend/dist`
    : `${process.cwd()}/frontend/`;

const app = express();

// Apply CORS with default settings, allowing all origins
app.use(cors());

// Set up Shopify authentication and webhook handling
app.get(shopify.config.auth.path, shopify.auth.begin());
app.get(
  shopify.config.auth.callbackPath,
  shopify.auth.callback(),
  shopify.redirectToShopifyOrAppRoot()
);
app.post(
  shopify.config.webhooks.path,
  shopify.processWebhooks({ webhookHandlers: PrivacyWebhookHandlers })
);

app.use("/api/*", shopify.validateAuthenticatedSession());

app.use(express.json());

//>>>>>>> APPROACH 1: Secure API Calls with `validateAuthenticatedSession`
app.get("/api/orders", async (_req, res) => {
  try {
    const client = new shopify.api.clients.Graphql({
      session: res.locals.shopify.session,
    });

    const countData = await client.request(`
      query shopifyOrderCount {
        ordersCount {
          count
        }
      }
    `);

    res.status(200).send({ count: countData.data.ordersCount.count });
  } catch (error) {
    console.log("ORDER COUNT ERROR", error);
  }
});

//>>>>>>> APPROACH 2: Direct Webhook Integration for External API Payloads
//SAMPLE RESPONSE

//  ---SESSION [
//    Session {
//      id: 'offline_carrier-delivery.myshopify.com',
//      shop: 'carrier-delivery.myshopify.com',
//      state: '236767996532294',
//      isOnline: false,
//      scope: 'read_customers,read_fulfillments,read_locations,write_assigned_fulfillment_orders,write_merchant_managed_fulfillment_orders,write_orders,write_products,write_shipping,write_third_party_fulfillment_orders',
//      expires: undefined,
//      accessToken: 'shpua_daae5162f4129b213b0f9dceb983880d',
//      onlineAccessInfo: undefined
//    }
//  ]
// error HttpResponseError: Received an error response (401 Unauthorized) from Shopify:
// {
//   "networkStatusCode": 401,
//   "message": "GraphQL Client: Unauthorized",
//   "response": {
//     "size": 0,
//     "timeout": 0
//   }
// }
app.post("/delivery", async (req, res) => {
  try {
    const { shop } = req.query; // Extract the shop domain from query parameters
    if (!shop) {
      return res.status(400).send({ error: "Shop domain is required." });
    }

    const sessionId = await shopify.api.session.getOfflineId(shop);
    const session = await shopify.config.sessionStorage.loadSession(sessionId);
    console.log("---SESSION", session);

    const client = new shopify.api.clients.Graphql({
      session: session,
    });

    const orderData = await client.request(`
      query shopifyOrders {
        orders(first: 5) {
          edges {
            node {
              id
              name
              totalPrice
            }
          }
        }
      }
    `);
    console.log("ORDER DATA", orderData);

    res.status(200).send({ orders: orderData.data.orders.edges });
  } catch (error) {
    console.log("error", error);
  }
});

app.use(shopify.cspHeaders());
app.use(serveStatic(STATIC_PATH, { index: false }));

app.use("/*", shopify.ensureInstalledOnShop(), async (_req, res, _next) => {
  return res
    .status(200)
    .set("Content-Type", "text/html")
    .send(
      readFileSync(join(STATIC_PATH, "index.html"))
        .toString()
        .replace("%VITE_SHOPIFY_API_KEY%", process.env.SHOPIFY_API_KEY || "")
    );
});

app.listen(PORT);

shopify.js

import { BillingInterval, LATEST_API_VERSION } from "@shopify/shopify-api";
import { shopifyApp } from "@shopify/shopify-app-express";
import { SQLiteSessionStorage } from "@shopify/shopify-app-session-storage-sqlite";
import { restResources } from "@shopify/shopify-api/rest/admin/2024-10";

const DB_PATH = `${process.cwd()}/database.sqlite`;

// The transactions with Shopify will always be marked as test transactions, unless NODE_ENV is production.
// See the ensureBilling helper to learn more about billing in this template.
const billingConfig = {
  "My Shopify One-Time Charge": {
    // This is an example configuration that would do a one-time charge for $5 (only USD is currently supported)
    amount: 5.0,
    currencyCode: "USD",
    interval: BillingInterval.OneTime,
  },
};

const shopify = shopifyApp({
  api: {
    apiVersion: LATEST_API_VERSION,
    restResources,
    future: {
      customerAddressDefaultFix: true,
      lineItemBilling: true,
      unstable_managedPricingSupport: true,
    },
    billing: undefined, // or replace with billingConfig above to enable example billing
  },
  auth: {
    path: "/api/auth",
    callbackPath: "/api/auth/callback",
  },
  webhooks: {
    path: "/api/webhooks",
  },
  // This should be replaced with your preferred storage strategy
  sessionStorage: new SQLiteSessionStorage(DB_PATH),
});
 
export default shopify;

Upvotes: 1

Views: 34

Answers (0)

Related Questions