{"_id":"@daniyal741/shopify-multistore-app-middleware","name":"@daniyal741/shopify-multistore-app-middleware","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@daniyal741/shopify-multistore-app-middleware","version":"0.1.0","description":"Enable custom app to be used in multiple shopify stores","main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=12.0.0"},"repository":{"type":"git","url":"git+https://github.com/daniyal741/shopify-multistore-app-middleware.git"},"exports":{".":{"require":"./dist/index.js","types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc","prepublishOnly":"npm run build","test":"echo \"no tests available\""},"author":{"name":"Pawel Jedrzejczyk"},"dependencies":{"@types/express":"^4.17.17","async":"^3.2.4"},"devDependencies":{"@shopify/shopify-api":"^11.0.0","@shopify/shopify-app-express":"^5.0.0","@types/async":"^3.2.20","typescript":"^4.9.4"},"peerDependencies":{"@shopify/shopify-api":"^11.0.0","@shopify/shopify-app-express":"^5.0.0"},"license":"ISC","publishConfig":{"access":"public"},"np":{"yarn":false,"contents":"dist"},"_id":"@daniyal741/shopify-multistore-app-middleware@0.1.0","gitHead":"f45d6bf72d61742da41453ead0ea520eded0fcb5","bugs":{"url":"https://github.com/daniyal741/shopify-multistore-app-middleware/issues"},"homepage":"https://github.com/daniyal741/shopify-multistore-app-middleware#readme","_nodeVersion":"22.3.0","_npmVersion":"10.8.1","dist":{"integrity":"sha512-D8mXR+wNt3l1rMbP1bkrt7/7d5hIwwsCTqNwJFGX6jvePO9JF49ArPEKcrZS2oiFTUAJHn+qgbOtmgAa0Nktkg==","shasum":"b81f62748a4a447a45401a6805842b0674645d74","tarball":"https://registry.npmjs.org/@daniyal741/shopify-multistore-app-middleware/-/shopify-multistore-app-middleware-0.1.0.tgz","fileCount":4,"unpackedSize":14677,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDU/Oh11YxzAQnS3kKukV1A5fgRyqpLFaxa/eil3+hZ5wIhAMPbEymi5t1u6U/a+qaSHFN1OAWY9Mcw76GTc4ELXeTh"}]},"_npmUser":{"name":"daniyal741","email":"bdani261@gmail.com"},"directories":{},"maintainers":[{"name":"daniyal741","email":"bdani261@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/shopify-multistore-app-middleware_0.1.0_1723514209967_0.1745660797072155"},"_hasShrinkwrap":false}},"time":{"created":"2024-08-13T01:56:49.890Z","0.1.0":"2024-08-13T01:56:50.135Z","modified":"2024-08-13T01:56:50.388Z"},"maintainers":[{"name":"daniyal741","email":"bdani261@gmail.com"}],"description":"Enable custom app to be used in multiple shopify stores","homepage":"https://github.com/daniyal741/shopify-multistore-app-middleware#readme","repository":{"type":"git","url":"git+https://github.com/daniyal741/shopify-multistore-app-middleware.git"},"author":{"name":"Pawel Jedrzejczyk"},"bugs":{"url":"https://github.com/daniyal741/shopify-multistore-app-middleware/issues"},"license":"ISC","readme":"# Shopify Multistore App Middleware\nThis a modified version of ``` shopify-multistore-app-middleware``` created by [Pawel Jedrzejczyk](https://github.com/paweljedrzejczyk/shopify-multistore-app-middleware)\n\n## Basic Requirements\n\n- Multiple Shopify Stores,\n- Shopify App Created in Partners Account - one for each store - this is where we get unique API_KEY and API_SECRET from,\n- Each App needs to have the URL configured to point to the same URL where we have our App deployed, so multiple apps but pointing to same URL,\n\n## Frontend changes requirements\n\nThe package is intented to allow the backend to work with multiple stores, but some frontend changes are needed as well.\n\nUpdate vite config to pass additional env vars with API Keys for multiple stores to frontend code:\n```tsx\nimport { defineConfig } from \"vite\";\nimport { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport https from \"https\";\nimport react from \"@vitejs/plugin-react\";\n\nif (\n  process.env.npm_lifecycle_event === \"build\" &&\n  !process.env.CI &&\n  !process.env.SHOPIFY_API_KEY &&\n  !Object.keys(process.env).some(\n    (key) => key.startsWith(\"SHOPIFY_API_KEY_\") && process.env[key]\n  )\n) {\n  console.warn(\n    \"\\nBuilding the frontend app without an API key. The frontend build will not run without an API key. Set the SHOPIFY_API_KEY environment variable when running the build command.\\n\"\n  );\n}\n\nconst proxyOptions = {\n  target: `http://127.0.0.1:${process.env.BACKEND_PORT}`,\n  changeOrigin: false,\n  secure: true,\n  ws: false,\n};\n\nconst host = process.env.HOST\n  ? process.env.HOST.replace(/https?:\\/\\//, \"\")\n  : \"localhost\";\n\nlet hmrConfig;\nif (host === \"localhost\") {\n  hmrConfig = {\n    protocol: \"ws\",\n    host: \"localhost\",\n    port: 64999,\n    clientPort: 64999,\n  };\n} else {\n  hmrConfig = {\n    protocol: \"wss\",\n    host: host,\n    port: process.env.FRONTEND_PORT,\n    clientPort: 443,\n  };\n}\n\nfunction getEnvs() {\n  const envs = {};\n  Object.keys(process.env).forEach((env) => {\n    if (env.startsWith(\"SHOPIFY_API_KEY_\")) {\n      envs[env] = process.env[env];\n    }\n  });\n\n  return {\n    \"process.env.SHOPIFY_API_KEYS\": JSON.stringify(envs),\n    \"process.env.SHOPIFY_API_KEY\": JSON.stringify(process.env.SHOPIFY_API_KEY),\n  };\n}\n\nexport default defineConfig({\n  root: dirname(fileURLToPath(import.meta.url)),\n  plugins: [react()],\n  define: getEnvs(),\n  resolve: {\n    preserveSymlinks: true,\n  },\n  server: {\n    host: \"localhost\",\n    port: process.env.FRONTEND_PORT,\n    hmr: hmrConfig,\n    proxy: {\n      \"^/(\\\\?.*)?$\": proxyOptions,\n      \"^/api(/|(\\\\?.*)?$)\": proxyOptions,\n    },\n  },\n});\n```\n\nUpdate AppBridgeProvider.tsx to use different API Key based on the shop:\n```tsx\nimport { useMemo, useState } from \"react\";\nimport { useLocation, useNavigate } from \"react-router-dom\";\nimport { Provider } from \"@shopify/app-bridge-react\";\nimport { Banner, Layout, Page } from \"@shopify/polaris\";\n\nconst sanitizedShopName = (shop: string): string =>\n  shop\n    .replace(/^https:\\/\\//, \"\")\n    .replace(\".myshopify.com\", \"\")\n    .replace(\"/admin\", \"\")\n    .replace(/-/g, \"_\")\n    .toUpperCase();\n\n\ndeclare global {\n  interface Window {\n    __SHOPIFY_DEV_HOST: string;\n  }\n}\n\n/**\n * A component to configure App Bridge.\n * @desc A thin wrapper around AppBridgeProvider that provides the following capabilities:\n *\n * 1. Ensures that navigating inside the app updates the host URL.\n * 2. Configures the App Bridge Provider, which unlocks functionality provided by the host.\n *\n * See: https://shopify.dev/apps/tools/app-bridge/react-components\n */\nexport function AppBridgeProvider({ children }: any) {\n  const location = useLocation();\n  const navigate = useNavigate();\n  const history = useMemo(\n    () => ({\n      replace: (path: any) => {\n        navigate(path, { replace: true });\n      },\n    }),\n    [navigate]\n  );\n\n  const routerConfig = useMemo(\n    () => ({ history, location }),\n    [history, location]\n  );\n\n  // The host may be present initially, but later removed by navigation.\n  // By caching this in state, we ensure that the host is never lost.\n  // During the lifecycle of an app, these values should never be updated anyway.\n  // Using state in this way is preferable to useMemo.\n  // See: https://stackoverflow.com/questions/60482318/version-of-usememo-for-caching-a-value-that-will-never-change\n  const [appBridgeConfig] = useState(() => {\n    const host =\n      new URLSearchParams(location.search).get(\"host\") ||\n      window.__SHOPIFY_DEV_HOST;\n\n    window.__SHOPIFY_DEV_HOST = host;\n\n    const customApiKey = process.env.SHOPIFY_API_KEYS?.[\n      `SHOPIFY_API_KEY_${sanitizedShopName(\n        host ? window.atob(host) : \"\"\n      )}` as keyof typeof process.env.SHOPIFY_API_KEYS\n    ] as string;\n\n    return {\n      host,\n      apiKey: customApiKey || process.env.SHOPIFY_API_KEY || \"\",\n      forceRedirect: true,\n    };\n  });\n\n  if (!appBridgeConfig.apiKey || !appBridgeConfig.host) {\n    const bannerProps = !appBridgeConfig.apiKey\n      ? {\n          title: \"Missing Shopify API Key\",\n          children: (\n            <>\n              Your app is running without the SHOPIFY_API_KEY environment\n              variable. Please ensure that it is set when running or building\n              your React app.\n            </>\n          ),\n        }\n      : {\n          title: \"Missing host query argument\",\n          children: (\n            <>\n              Your app can only load if the URL has a <b>host</b> argument.\n              Please ensure that it is set, or access your app using the\n              Partners Dashboard <b>Test your app</b> feature\n            </>\n          ),\n        };\n\n    return (\n      <Page narrowWidth>\n        <Layout>\n          <Layout.Section>\n            <div style={{ marginTop: \"100px\" }}>\n              <Banner {...bannerProps} status=\"critical\" />\n            </div>\n          </Layout.Section>\n        </Layout>\n      </Page>\n    );\n  }\n\n  return (\n    <Provider config={appBridgeConfig} router={routerConfig}>\n      {children}\n    </Provider>\n  );\n}\n```\n\n## Usage:\n\n```\nnpm install @paweljedrzejczyk/shopify-multistore-app-middleware\n```\n\n### createMultistoreMiddleware\n\n```js\nimport express from \"express\";\nimport { createMultistoreMiddleware } from \"@paweljedrzejczyk/shopify-multistore-app-middleware\";\n\nconst app = express();\n\napp.use(createMultistoreMiddleware(createShopifyApp));\n```\n\nwhere `createShopifyApp` is a function you declare that will accept the `(shop: string)` argument and return `ShopifyApp` instance created with `@shopify/shopify-app-express`. Shop passed as argument is sanitized to be used within the process.env variable name so it's also uppercased etc.\n\nConsider we are connecting from `my-dummy-store-1.myshopify.com` store, the store argument will be: `MY_DUMMY_STORE_1`. The `createShopifyApp` function is up to us how we want to have it, but we can allow multiple shops by exposing process.env variables like:\n\n```\nprocess.env.SHOPIFY_API_KEY_MY_DUMMY_STORE_1 = 'XYZ';\nprocess.env.SHOPIFY_API_SECRET_MY_DUMMY_STORE_1 = 'XYZ';\n```\n\nexample:\n\n```ts\nimport { LATEST_API_VERSION } from \"@shopify/shopify-api\";\nimport { ShopifyApp, shopifyApp } from \"@shopify/shopify-app-express\";\nimport { AppConfigParams } from \"@shopify/shopify-app-express/build/ts/config-types\";\nimport { PostgreSQLSessionStorage } from \"@shopify/shopify-app-session-storage-postgresql\";\nlet { restResources } = await import(\n  `@shopify/shopify-api/rest/admin/${LATEST_API_VERSION}`\n);\n\nexport const shopifyAppConfig: Pick<AppConfigParams, \"auth\" | \"webhooks\"> = {\n  auth: {\n    path: \"/api/auth\",\n    callbackPath: \"/api/auth/callback\",\n  },\n  webhooks: {\n    path: \"/api/webhooks\",\n  },\n};\n\nexport const createShopifyApp = (shop: string): ShopifyApp => {\n  const app = shopifyApp({\n    api: {\n      apiVersion: LATEST_API_VERSION,\n      restResources,\n      apiKey: process.env[`SHOPIFY_API_KEY_${shopName}`],\n      apiSecretKey: process.env[`SHOPIFY_API_SECRET_${shopName}`],\n    },\n    auth: shopifyAppConfig.auth,\n    webhooks: shopifyAppConfig.webhooks,\n    sessionStorage: new PostgreSQLSessionStorage(\n      new URL(process.env.DATABASE_URL)\n    ),\n  });\n\n  return app;\n};\n```\n\nIn the example above multiple stores will be using different shopify API keys, but same postgres database. We can rewrite it to connect to separate database for each store if needed.\n\n### useShopifyApp\n\n`ShopifyApp` instance is later stored in `res.locals.shopify.app` to be used in the request handlers, this is where `useShopifyApp` comes handy.\n\nWe can rewrite:\n\n```js\napp.get(shopifyAppConfig.auth.path, (req, res, next) => {\n  return res.locals.shopify.app.auth.begin()(res, res, next);\n});\n```\n\nto:\n\n```js\napp.get(\n  shopifyAppConfig.auth.path,\n  useShopifyApp((shopifyApp) => shopifyApp.auth.begin())\n);\n```\n\n### getShopifyApp\n\nThis function is just a shortcut for accessing `res.locals.shopify.app` but it does return proper typing. Used to access the shopify app within the request handler.\n\nExample:\n\n```js\napp.post(\n  \"/api/graphql\",\n  useShopifyApp((shopifyApp) => shopifyApp.validateAuthenticatedSession()),\n  async (req, res) => {\n    try {\n      const response = await getShopifyApp(res).api.clients.graphqlProxy({\n        session: res.locals.shopify.session,\n        rawBody: req.body,\n      });\n      return res.status(200).send(response.body);\n    } catch (error: unknown) {\n      return res.status(500).send(error);\n    }\n  }\n);\n```\n","readmeFilename":"README.md"}