Initial commit: Adding the template

This commit is contained in:
Albez0-An7h 2026-03-04 13:27:19 +05:30
commit a74a89109d
43 changed files with 13869 additions and 0 deletions

8
.cursor/mcp.json Normal file
View File

@ -0,0 +1,8 @@
{
"mcpServers": {
"shopify-dev-mcp": {
"command": "npx",
"args": ["-y", "@shopify/dev-mcp@latest"]
}
}
}

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
.cache
build
node_modules

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
# Markdown syntax specifies that trailing whitespaces can be meaningful,
# so lets not trim those. e.g. 2 trailing spaces = linebreak (<br />)
# See https://daringfireball.net/projects/markdown/syntax#p
[*.md]
trim_trailing_whitespace = false

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
node_modules
build
public/build
*/*.yml
.shopify

96
.eslintrc.cjs Normal file
View File

@ -0,0 +1,96 @@
/**
* This is intended to be a basic starting point for linting in your app.
* It relies on recommended configs out of the box for simplicity, but you can
* and should modify this configuration to best suit your team's needs.
*/
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
commonjs: true,
es6: true,
},
ignorePatterns: ["!**/.server", "!**/.client"],
// Base config
extends: ["eslint:recommended"],
overrides: [
// React
{
files: ["**/*.{js,jsx,ts,tsx}"],
plugins: ["react", "jsx-a11y"],
extends: [
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
],
settings: {
react: {
version: "detect",
},
formComponents: ["Form"],
linkComponents: [
{ name: "Link", linkAttribute: "to" },
{ name: "NavLink", linkAttribute: "to" },
],
"import/resolver": {
typescript: {},
},
},
rules: {
"react/no-unknown-property": ["error", { ignore: ["variant"] }],
},
},
// Typescript
{
files: ["**/*.{ts,tsx}"],
plugins: ["@typescript-eslint", "import"],
parser: "@typescript-eslint/parser",
settings: {
"import/internal-regex": "^~/",
"import/resolver": {
node: {
extensions: [".ts", ".tsx"],
},
typescript: {
alwaysTryTypes: true,
},
},
},
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
},
// Node
{
files: [
".eslintrc.cjs",
"vite.config.{js,ts}",
".graphqlrc.{js,ts}",
"shopify.server.{js,ts}",
"**/*.server.{js,ts}",
],
env: {
node: true,
},
},
],
globals: {
shopify: "readonly"
},
};

View File

@ -0,0 +1,10 @@
{
"name": "shopify-dev-mcp",
"version": "1.0.0",
"mcpServers": {
"shopify-dev-mcp": {
"command": "npx",
"args": ["-y", "@shopify/dev-mcp@latest"]
}
}
}

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
node_modules
# macOS
.DS_Store
/.cache
/build
/app/build
/public/build/
/public/_dev
/app/public/build
/prisma/dev.sqlite
/prisma/dev.sqlite-journal
database.sqlite
.env
.env.*
/extensions/*/dist
# Ignore shopify files created during app dev
.shopify/*
.shopify.lock
# Hide files auto-generated by react router
.react-router/

38
.graphqlrc.js Normal file
View File

@ -0,0 +1,38 @@
import fs from "fs";
import { ApiVersion } from "@shopify/shopify-app-react-router/server";
import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
function getConfig() {
const config = {
projects: {
default: shopifyApiProject({
apiType: ApiType.Admin,
apiVersion: ApiVersion.October25,
documents: [
"./app/**/*.{js,ts,jsx,tsx}",
"./app/.server/**/*.{js,ts,jsx,tsx}",
],
outputDir: "./app/types",
}),
},
};
let extensions = [];
try {
extensions = fs.readdirSync("./extensions");
} catch {
// ignore if no extensions
}
for (const entry of extensions) {
const extensionPath = `./extensions/${entry}`;
const schema = `${extensionPath}/schema.graphql`;
if (!fs.existsSync(schema)) {
continue;
}
config.projects[entry] = {
schema,
documents: [`${extensionPath}/**/*.graphql`],
};
}
return config;
}
const config = getConfig();
export default config;

9
.mcp.json Normal file
View File

@ -0,0 +1,9 @@
{
"mcpServers": {
"shopify-dev-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@shopify/dev-mcp@latest"]
}
}
}

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
engine-strict=true
shamefully-hoist=true

7
.prettierignore Normal file
View File

@ -0,0 +1,7 @@
package.json
.shadowenv.d
.vscode
node_modules
prisma
public
.shopify

125
CHANGELOG.md Normal file
View File

@ -0,0 +1,125 @@
# @shopify/shopify-app-template-react-router
## 2026.02.09
- Add declarative product metafield definition and demonstrate metafield usage in the product creation flow
- Add declarative metaobject definition and demonstrate metaobject upsert in the product creation flow
## 2026.01.08
- [#170](https://github.com/Shopify/shopify-app-template-react-router/pull/170) - Update React Router minimum version to v7.12.0
## 2025.12.11
- [#151](https://github.com/Shopify/shopify-app-template-react-router/pull/151) Update `@shopify/shopify-app-react-router` to v1.1.0 and `@shopify/shopify-app-session-storage-prisma` to v8.0.0, add refresh token fields (`refreshToken` and `refreshTokenExpires`) to Session model in Prisma schema, and adopt the `expiringOfflineAccessTokens` flag for enhanced security through token rotation. See [expiring vs non-expiring offline tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens#expiring-vs-non-expiring-offline-tokens) for more information.
## 2025.10.10
- [#95](https://github.com/Shopify/shopify-app-template-react-router/pull/95) Swap the product link for [admin intents](https://shopify.dev/docs/apps/build/admin/admin-intents).
## 2025.10.02
- [#81](https://github.com/Shopify/shopify-app-template-react-router/pull/81) Add shopify global to eslint for ui extensions
## 2025.10.01
- [#79](https://github.com/Shopify/shopify-app-template-react-router/pull/78) Update API version to 2025-10.
- [#77](https://github.com/Shopify/shopify-app-template-react-router/pull/77) Update `@shopify/shopify-app-react-router` to V1.
- [#73](https://github.com/Shopify/shopify-app-template-react-router/pull/73/files) Rename @shopify/app-bridge-ui-types to @shopify/polaris-types
## 2025.08.30
- [#70](https://github.com/Shopify/shopify-app-template-react-router/pull/70/files) Upgrade `@shopify/app-bridge-ui-types` from 0.2.1 to 0.3.1.
## 2025.08.17
- [#58](https://github.com/Shopify/shopify-app-template-react-router/pull/58) Update Shopify & React Router dependencies. Use Shopify React Router in graphqlrc, not shopify-api
- [#57](https://github.com/Shopify/shopify-app-template-react-router/pull/57) Update Webhook API version in `shopify.app.toml` to `2025-07`
- [#56](https://github.com/Shopify/shopify-app-template-react-router/pull/56) Remove local CLI from package.json in favor of global CLI installation
- [#53](https://github.com/Shopify/shopify-app-template-react-router/pull/53) Add the Shopify Dev MCP to the template
## 2025.08.16
- [#52](https://github.com/Shopify/shopify-app-template-react-router/pull/52) Use `ApiVersion.July25` rather than `LATEST_API_VERSION` in `.graphqlrc`.
## 2025.07.24
- [14](https://github.com/Shopify/shopify-app-template-react-router/pull/14/files) Add [App Bridge web components](https://shopify.dev/docs/api/app-home/app-bridge-web-components) to the template.
## July 2025
Forked the [shopify-app-template repo](https://github.com/Shopify/shopify-app-template-remix)
# @shopify/shopify-app-template-remix
## 2025.03.18
-[#998](https://github.com/Shopify/shopify-app-template-remix/pull/998) Update to Vite 6
## 2025.03.01
- [#982](https://github.com/Shopify/shopify-app-template-remix/pull/982) Add Shopify Dev Assistant extension to the VSCode extension recommendations
## 2025.01.31
- [#952](https://github.com/Shopify/shopify-app-template-remix/pull/952) Update to Shopify App API v2025-01
## 2025.01.23
- [#923](https://github.com/Shopify/shopify-app-template-remix/pull/923) Update `@shopify/shopify-app-session-storage-prisma` to v6.0.0
## 2025.01.8
- [#923](https://github.com/Shopify/shopify-app-template-remix/pull/923) Enable GraphQL autocomplete for Javascript
## 2024.12.19
- [#904](https://github.com/Shopify/shopify-app-template-remix/pull/904) bump `@shopify/app-bridge-react` to latest
-
## 2024.12.18
- [875](https://github.com/Shopify/shopify-app-template-remix/pull/875) Add Scopes Update Webhook
## 2024.12.05
- [#910](https://github.com/Shopify/shopify-app-template-remix/pull/910) Install `openssl` in Docker image to fix Prisma (see [#25817](https://github.com/prisma/prisma/issues/25817#issuecomment-2538544254))
- [#907](https://github.com/Shopify/shopify-app-template-remix/pull/907) Move `@remix-run/fs-routes` to `dependencies` to fix Docker image build
- [#899](https://github.com/Shopify/shopify-app-template-remix/pull/899) Disable v3_singleFetch flag
- [#898](https://github.com/Shopify/shopify-app-template-remix/pull/898) Enable the `removeRest` future flag so new apps aren't tempted to use the REST Admin API.
## 2024.12.04
- [#891](https://github.com/Shopify/shopify-app-template-remix/pull/891) Enable remix future flags.
## 2024.11.26
- [888](https://github.com/Shopify/shopify-app-template-remix/pull/888) Update restResources version to 2024-10
## 2024.11.06
- [881](https://github.com/Shopify/shopify-app-template-remix/pull/881) Update to the productCreate mutation to use the new ProductCreateInput type
## 2024.10.29
- [876](https://github.com/Shopify/shopify-app-template-remix/pull/876) Update shopify-app-remix to v3.4.0 and shopify-app-session-storage-prisma to v5.1.5
## 2024.10.02
- [863](https://github.com/Shopify/shopify-app-template-remix/pull/863) Update to Shopify App API v2024-10 and shopify-app-remix v3.3.2
## 2024.09.18
- [850](https://github.com/Shopify/shopify-app-template-remix/pull/850) Removed "~" import alias
## 2024.09.17
- [842](https://github.com/Shopify/shopify-app-template-remix/pull/842) Move webhook processing to individual routes
## 2024.08.19
Replaced deprecated `productVariantUpdate` with `productVariantsBulkUpdate`
## v2024.08.06
Allow `SHOP_REDACT` webhook to process without admin context
## v2024.07.16
Started tracking changes and releases using calver

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM node:20-alpine
RUN apk add --no-cache openssl
EXPOSE 3000
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev && npm cache clean --force
COPY . .
RUN npm run build
CMD ["npm", "run", "docker-start"]

237
README.md Normal file
View File

@ -0,0 +1,237 @@
# Shopify App Template - React Router
This is a template for building a [Shopify app](https://shopify.dev/docs/apps/getting-started) using [React Router](https://reactrouter.com/). It was forked from the [Shopify Remix app template](https://github.com/Shopify/shopify-app-template-remix) and converted to React Router.
Rather than cloning this repo, follow the [Quick Start steps](https://github.com/Shopify/shopify-app-template-react-router#quick-start).
Visit the [`shopify.dev` documentation](https://shopify.dev/docs/api/shopify-app-react-router) for more details on the React Router app package.
## Upgrading from Remix
If you have an existing Remix app that you want to upgrade to React Router, please follow the [upgrade guide](https://github.com/Shopify/shopify-app-template-react-router/wiki/Upgrading-from-Remix). Otherwise, please follow the quick start guide below.
## Quick start
### Prerequisites
Before you begin, you'll need to [download and install the Shopify CLI](https://shopify.dev/docs/apps/tools/cli/getting-started) if you haven't already.
### Setup
```shell
shopify app init --template=https://github.com/Shopify/shopify-app-template-react-router
```
### Local Development
```shell
shopify app dev
```
Press P to open the URL to your app. Once you click install, you can start development.
Local development is powered by [the Shopify CLI](https://shopify.dev/docs/apps/tools/cli). It logs into your account, connects to an app, provides environment variables, updates remote config, creates a tunnel and provides commands to generate extensions.
### Authenticating and querying data
To authenticate and query data you can use the `shopify` const that is exported from `/app/shopify.server.js`:
```js
export async function loader({ request }) {
const { admin } = await shopify.authenticate.admin(request);
const response = await admin.graphql(`
{
products(first: 25) {
nodes {
title
description
}
}
}`);
const {
data: {
products: { nodes },
},
} = await response.json();
return nodes;
}
```
This template comes pre-configured with examples of:
1. Setting up your Shopify app in [/app/shopify.server.ts](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/shopify.server.ts)
2. Querying data using Graphql. Please see: [/app/routes/app.\_index.tsx](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/routes/app._index.tsx).
3. Responding to webhooks. Please see [/app/routes/webhooks.tsx](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/routes/webhooks.app.uninstalled.tsx).
4. Using metafields, metaobjects, and declarative custom data definitions. Please see [/app/routes/app.\_index.tsx](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/routes/app._index.tsx) and [shopify.app.toml](https://github.com/Shopify/shopify-app-template-react-router/blob/main/shopify.app.toml).
Please read the [documentation for @shopify/shopify-app-react-router](https://shopify.dev/docs/api/shopify-app-react-router) to see what other API's are available.
## Shopify Dev MCP
This template is configured with the Shopify Dev MCP. This instructs [Cursor](https://cursor.com/), [GitHub Copilot](https://github.com/features/copilot) and [Claude Code](https://claude.com/product/claude-code) and [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) to use the Shopify Dev MCP.
For more information on the Shopify Dev MCP please read [the documentation](https://shopify.dev/docs/apps/build/devmcp).
## Deployment
### Application Storage
This template uses [Prisma](https://www.prisma.io/) to store session data, by default using an [SQLite](https://www.sqlite.org/index.html) database.
The database is defined as a Prisma schema in `prisma/schema.prisma`.
This use of SQLite works in production if your app runs as a single instance.
The database that works best for you depends on the data your app needs and how it is queried.
Heres a short list of databases providers that provide a free tier to get started:
| Database | Type | Hosters |
| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| MySQL | SQL | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-mysql), [Planet Scale](https://planetscale.com/), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Google Cloud SQL](https://cloud.google.com/sql/docs/mysql) |
| PostgreSQL | SQL | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-postgresql), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Google Cloud SQL](https://cloud.google.com/sql/docs/postgres) |
| Redis | Key-value | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-redis), [Amazon MemoryDB](https://aws.amazon.com/memorydb/) |
| MongoDB | NoSQL / Document | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-mongodb), [MongoDB Atlas](https://www.mongodb.com/atlas/database) |
To use one of these, you can use a different [datasource provider](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#datasource) in your `schema.prisma` file, or a different [SessionStorage adapter package](https://github.com/Shopify/shopify-api-js/blob/main/packages/shopify-api/docs/guides/session-storage.md).
### Build
Build the app by running the command below with the package manager of your choice:
Using yarn:
```shell
yarn build
```
Using npm:
```shell
npm run build
```
Using pnpm:
```shell
pnpm run build
```
## Hosting
When you're ready to set up your app in production, you can follow [our deployment documentation](https://shopify.dev/docs/apps/launch/deployment) to host it externally. From there, you have a few options:
- [Google Cloud Run](https://shopify.dev/docs/apps/launch/deployment/deploy-to-google-cloud-run): This tutorial is written specifically for this example repo, and is compatible with the extended steps included in the subsequent [**Build your app**](tutorial) in the **Getting started** docs. It is the most detailed tutorial for taking a React Router-based Shopify app and deploying it to production. It includes configuring permissions and secrets, setting up a production database, and even hosting your apps behind a load balancer across multiple regions.
- [Fly.io](https://fly.io/docs/js/shopify/): Leverages the Fly.io CLI to quickly launch Shopify apps to a single machine.
- [Render](https://render.com/docs/deploy-shopify-app): This tutorial guides you through using Docker to deploy and install apps on a Dev store.
- [Manual deployment guide](https://shopify.dev/docs/apps/launch/deployment/deploy-to-hosting-service): This resource provides general guidance on the requirements of deployment including environment variables, secrets, and persistent data.
When you reach the step for [setting up environment variables](https://shopify.dev/docs/apps/deployment/web#set-env-vars), you also need to set the variable `NODE_ENV=production`.
## Gotchas / Troubleshooting
### Database tables don't exist
If you get an error like:
```
The table `main.Session` does not exist in the current database.
```
Create the database for Prisma. Run the `setup` script in `package.json` using `npm`, `yarn` or `pnpm`.
### Navigating/redirecting breaks an embedded app
Embedded apps must maintain the user session, which can be tricky inside an iFrame. To avoid issues:
1. Use `Link` from `react-router` or `@shopify/polaris`. Do not use `<a>`.
2. Use `redirect` returned from `authenticate.admin`. Do not use `redirect` from `react-router`
3. Use `useSubmit` from `react-router`.
This only applies if your app is embedded, which it will be by default.
### Webhooks: shop-specific webhook subscriptions aren't updated
If you are registering webhooks in the `afterAuth` hook, using `shopify.registerWebhooks`, you may find that your subscriptions aren't being updated.
Instead of using the `afterAuth` hook declare app-specific webhooks in the `shopify.app.toml` file. This approach is easier since Shopify will automatically sync changes every time you run `deploy` (e.g: `npm run deploy`). Please read these guides to understand more:
1. [app-specific vs shop-specific webhooks](https://shopify.dev/docs/apps/build/webhooks/subscribe#app-specific-subscriptions)
2. [Create a subscription tutorial](https://shopify.dev/docs/apps/build/webhooks/subscribe/get-started?deliveryMethod=https)
If you do need shop-specific webhooks, keep in mind that the package calls `afterAuth` in 2 scenarios:
- After installing the app
- When an access token expires
During normal development, the app won't need to re-authenticate most of the time, so shop-specific subscriptions aren't updated. To force your app to update the subscriptions, uninstall and reinstall the app. Revisiting the app will call the `afterAuth` hook.
### Webhooks: Admin created webhook failing HMAC validation
Webhooks subscriptions created in the [Shopify admin](https://help.shopify.com/en/manual/orders/notifications/webhooks) will fail HMAC validation. This is because the webhook payload is not signed with your app's secret key.
The recommended solution is to use [app-specific webhooks](https://shopify.dev/docs/apps/build/webhooks/subscribe#app-specific-subscriptions) defined in your toml file instead. Test your webhooks by triggering events manually in the Shopify admin(e.g. Updating the product title to trigger a `PRODUCTS_UPDATE`).
### Webhooks: Admin object undefined on webhook events triggered by the CLI
When you trigger a webhook event using the Shopify CLI, the `admin` object will be `undefined`. This is because the CLI triggers an event with a valid, but non-existent, shop. The `admin` object is only available when the webhook is triggered by a shop that has installed the app. This is expected.
Webhooks triggered by the CLI are intended for initial experimentation testing of your webhook configuration. For more information on how to test your webhooks, see the [Shopify CLI documentation](https://shopify.dev/docs/apps/tools/cli/commands#webhook-trigger).
### Incorrect GraphQL Hints
By default the [graphql.vscode-graphql](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) extension for will assume that GraphQL queries or mutations are for the [Shopify Admin API](https://shopify.dev/docs/api/admin). This is a sensible default, but it may not be true if:
1. You use another Shopify API such as the storefront API.
2. You use a third party GraphQL API.
If so, please update [.graphqlrc.ts](https://github.com/Shopify/shopify-app-template-react-router/blob/main/.graphqlrc.ts).
### Using Defer & await for streaming responses
By default the CLI uses a cloudflare tunnel. Unfortunately cloudflare tunnels wait for the Response stream to finish, then sends one chunk. This will not affect production.
To test [streaming using await](https://reactrouter.com/api/components/Await#await) during local development we recommend [localhost based development](https://shopify.dev/docs/apps/build/cli-for-apps/networking-options#localhost-based-development).
### "nbf" claim timestamp check failed
This is because a JWT token is expired. If you are consistently getting this error, it could be that the clock on your machine is not in sync with the server. To fix this ensure you have enabled "Set time and date automatically" in the "Date and Time" settings on your computer.
### Using MongoDB and Prisma
If you choose to use MongoDB with Prisma, there are some gotchas in Prisma's MongoDB support to be aware of. Please see the [Prisma SessionStorage README](https://www.npmjs.com/package/@shopify/shopify-app-session-storage-prisma#mongodb).
### Unable to require(`C:\...\query_engine-windows.dll.node`).
Unable to require(`C:\...\query_engine-windows.dll.node`).
The Prisma engines do not seem to be compatible with your system.
query_engine-windows.dll.node is not a valid Win32 application.
**Fix:** Set the environment variable:
```shell
PRISMA_CLIENT_ENGINE_TYPE=binary
```
This forces Prisma to use the binary engine mode, which runs the query engine as a separate process and can work via emulation on Windows ARM64.
## Resources
React Router:
- [React Router docs](https://reactrouter.com/home)
Shopify:
- [Intro to Shopify apps](https://shopify.dev/docs/apps/getting-started)
- [Shopify App React Router docs](https://shopify.dev/docs/api/shopify-app-react-router)
- [Shopify CLI](https://shopify.dev/docs/apps/tools/cli)
- [Shopify App Bridge](https://shopify.dev/docs/api/app-bridge-library).
- [Polaris Web Components](https://shopify.dev/docs/api/app-home/polaris-web-components).
- [App extensions](https://shopify.dev/docs/apps/app-extensions/list)
- [Shopify Functions](https://shopify.dev/docs/api/functions)
Internationalization:
- [Internationalizing your app](https://shopify.dev/docs/apps/best-practices/internationalization/getting-started)

11
app/db.server.js Normal file
View File

@ -0,0 +1,11 @@
import { PrismaClient } from "@prisma/client";
if (process.env.NODE_ENV !== "production") {
if (!global.prismaGlobal) {
global.prismaGlobal = new PrismaClient();
}
}
const prisma = global.prismaGlobal ?? new PrismaClient();
export default prisma;

51
app/entry.server.jsx Normal file
View File

@ -0,0 +1,51 @@
import { PassThrough } from "stream";
import { renderToPipeableStream } from "react-dom/server";
import { ServerRouter } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { isbot } from "isbot";
import { addDocumentResponseHeaders } from "./shopify.server";
export const streamTimeout = 5000;
export default async function handleRequest(
request,
responseStatusCode,
responseHeaders,
reactRouterContext,
) {
addDocumentResponseHeaders(request, responseHeaders);
const userAgent = request.headers.get("user-agent");
const callbackName = isbot(userAgent ?? "") ? "onAllReady" : "onShellReady";
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={reactRouterContext} url={request.url} />,
{
[callbackName]: () => {
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
pipe(body);
},
onShellError(error) {
reject(error);
},
onError(error) {
responseStatusCode = 500;
console.error(error);
},
},
);
// Automatically timeout the React renderer after 6 seconds, which ensures
// React has enough time to flush down the rejected boundary contents
setTimeout(abort, streamTimeout + 1000);
});
}

24
app/root.jsx Normal file
View File

@ -0,0 +1,24 @@
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="preconnect" href="https://cdn.shopify.com/" />
<link
rel="stylesheet"
href="https://cdn.shopify.com/static/fonts/inter/v4/styles.css"
/>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

3
app/routes.js Normal file
View File

@ -0,0 +1,3 @@
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes();

View File

@ -0,0 +1,54 @@
import { redirect, Form, useLoaderData } from "react-router";
import { login } from "../../shopify.server";
import styles from "./styles.module.css";
export const loader = async ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get("shop")) {
throw redirect(`/app?${url.searchParams.toString()}`);
}
return { showForm: Boolean(login) };
};
export default function App() {
const { showForm } = useLoaderData();
return (
<div className={styles.index}>
<div className={styles.content}>
<h1 className={styles.heading}>A short heading about [your app]</h1>
<p className={styles.text}>
A tagline about [your app] that describes your value proposition.
</p>
{showForm && (
<Form className={styles.form} method="post" action="/auth/login">
<label className={styles.label}>
<span>Shop domain</span>
<input className={styles.input} type="text" name="shop" />
<span>e.g: my-shop-domain.myshopify.com</span>
</label>
<button className={styles.button} type="submit">
Log in
</button>
</Form>
)}
<ul className={styles.list}>
<li>
<strong>Product feature</strong>. Some detail about your feature and
its benefit to your customer.
</li>
<li>
<strong>Product feature</strong>. Some detail about your feature and
its benefit to your customer.
</li>
<li>
<strong>Product feature</strong>. Some detail about your feature and
its benefit to your customer.
</li>
</ul>
</div>
</div>
);
}

View File

@ -0,0 +1,73 @@
.index {
align-items: center;
display: flex;
justify-content: center;
height: 100%;
width: 100%;
text-align: center;
padding: 1rem;
}
.heading,
.text {
padding: 0;
margin: 0;
}
.text {
font-size: 1.2rem;
padding-bottom: 2rem;
}
.content {
display: grid;
gap: 2rem;
}
.form {
display: flex;
align-items: center;
justify-content: flex-start;
margin: 0 auto;
gap: 1rem;
}
.label {
display: grid;
gap: 0.2rem;
max-width: 20rem;
text-align: left;
font-size: 1rem;
}
.input {
padding: 0.4rem;
}
.button {
padding: 0.4rem;
}
.list {
list-style: none;
padding: 0;
padding-top: 3rem;
margin: 0;
display: flex;
gap: 2rem;
}
.list > li {
max-width: 20rem;
text-align: left;
}
@media only screen and (max-width: 50rem) {
.list {
display: block;
}
.list > li {
padding-bottom: 1rem;
}
}

128
app/routes/app._index.jsx Normal file
View File

@ -0,0 +1,128 @@
import { useLoaderData, Link } from "react-router";
import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
import { getQRCodes } from "../models/QRCode.server";
export async function loader({ request }) {
const { admin, session } = await authenticate.admin(request);
const qrCodes = await getQRCodes(session.shop, admin.graphql);
return {
qrCodes,
};
}
const EmptyQRCodeState = () => (
<s-section accessibilityLabel="Empty state section">
<s-grid gap="base" justifyItems="center" paddingBlock="large-400">
<s-box maxInlineSize="200px" maxBlockSize="200px">
<s-image
aspectRatio="1/0.5"
src="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
alt="A stylized graphic of a document"
/>
</s-box>
<s-grid justifyItems="center" maxBlockSize="450px" maxInlineSize="450px">
<s-heading>Create unique QR codes for your products</s-heading>
<s-paragraph>
Allow customers to scan codes and buy products using their phones.
</s-paragraph>
<s-stack
gap="small-200"
justifyContent="center"
padding="base"
paddingBlockEnd="none"
direction="inline"
>
<s-button href="/app/qrcodes/new" variant="primary">
Create QR code
</s-button>
</s-stack>
</s-grid>
</s-grid>
</s-section>
);
function truncate(str, { length = 25 } = {}) {
if (!str) return "";
if (str.length <= length) return str;
return str.slice(0, length) + "…";
}
const QRTable = ({ qrCodes }) => (
<s-section padding="none" accessibilityLabel="QRCode table">
<s-table>
<s-table-header-row>
<s-table-header listSlot="primary">Title</s-table-header>
<s-table-header>Product</s-table-header>
<s-table-header>Date created</s-table-header>
<s-table-header>Scans</s-table-header>
</s-table-header-row>
<s-table-body>
{qrCodes.map((qrCode) => (
<QRTableRow key={qrCode.id} qrCode={qrCode} />
))}
</s-table-body>
</s-table>
</s-section>
);
const QRTableRow = ({ qrCode }) => (
<s-table-row id={qrCode.id} position={qrCode.id}>
<s-table-cell>
<s-stack direction="inline" gap="small" alignItems="center">
<s-clickable
href={`/app/qrcodes/${qrCode.id}`}
accessibilityLabel={`Go to the product page for ${qrCode.productTitle}`}
border="base"
borderRadius="base"
overflow="hidden"
inlineSize="20px"
blockSize="20px"
>
{qrCode.productImage ? (
<s-image objectFit="cover" src={qrCode.productImage}></s-image>
) : (
<s-icon size="large" type="image" />
)}
</s-clickable>
<s-link href={`/app/qrcodes/${qrCode.id}`}>
{truncate(qrCode.title)}
</s-link>
</s-stack>
</s-table-cell>
<s-table-cell>
{qrCode.productDeleted ? (
<s-badge icon="alert-diamond" tone="critical">
Product has been deleted
</s-badge>
) : (
truncate(qrCode.productTitle)
)}
</s-table-cell>
<s-table-cell>{new Date(qrCode.createdAt).toDateString()}</s-table-cell>
<s-table-cell>{qrCode.scans}</s-table-cell>
</s-table-row>
);
export default function Index() {
const { qrCodes } = useLoaderData();
return (
<s-page heading="QR codes">
<s-link slot="secondary-actions" href="/app/qrcodes/new">
Create QR code
</s-link>
{qrCodes.length === 0 ? (
<EmptyQRCodeState />
) : (
<QRTable qrCodes={qrCodes} />
)}
</s-page>
);
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};

View File

@ -0,0 +1,37 @@
export default function AdditionalPage() {
return (
<s-page heading="Additional page">
<s-section heading="Multiple pages">
<s-paragraph>
The app template comes with an additional page which demonstrates how
to create multiple pages within app navigation using{" "}
<s-link
href="https://shopify.dev/docs/apps/tools/app-bridge"
target="_blank"
>
App Bridge
</s-link>
.
</s-paragraph>
<s-paragraph>
To create your own page and have it show up in the app navigation, add
a page inside <code>app/routes</code>, and a link to it in the{" "}
<code>&lt;ui-nav-menu&gt;</code> component found in{" "}
<code>app/routes/app.jsx</code>.
</s-paragraph>
</s-section>
<s-section slot="aside" heading="Resources">
<s-unordered-list>
<s-list-item>
<s-link
href="https://shopify.dev/docs/apps/design-guidelines/navigation#app-nav"
target="_blank"
>
App nav best practices
</s-link>
</s-list-item>
</s-unordered-list>
</s-section>
</s-page>
);
}

34
app/routes/app.jsx Normal file
View File

@ -0,0 +1,34 @@
import { Outlet, useLoaderData, useRouteError } from "react-router";
import { boundary } from "@shopify/shopify-app-react-router/server";
import { AppProvider } from "@shopify/shopify-app-react-router/react";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
await authenticate.admin(request);
// eslint-disable-next-line no-undef
return { apiKey: process.env.SHOPIFY_API_KEY || "" };
};
export default function App() {
const { apiKey } = useLoaderData();
return (
<AppProvider embedded apiKey={apiKey}>
<s-app-nav>
<s-link href="/app">Home</s-link>
<s-link href="/app/additional">Additional page</s-link>
</s-app-nav>
<Outlet />
</AppProvider>
);
}
// Shopify needs React Router to catch some thrown responses, so that their headers are included in the response.
export function ErrorBoundary() {
return boundary.error(useRouteError());
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};

12
app/routes/auth.$.jsx Normal file
View File

@ -0,0 +1,12 @@
import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }) => {
await authenticate.admin(request);
return null;
};
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
};

View File

@ -0,0 +1,11 @@
import { LoginErrorType } from "@shopify/shopify-app-react-router/server";
export function loginErrorMessage(loginErrors) {
if (loginErrors?.shop === LoginErrorType.MissingShop) {
return { shop: "Please enter your shop domain to log in" };
} else if (loginErrors?.shop === LoginErrorType.InvalidShop) {
return { shop: "Please enter a valid shop domain to log in" };
}
return {};
}

View File

@ -0,0 +1,47 @@
import { AppProvider } from "@shopify/shopify-app-react-router/react";
import { useState } from "react";
import { Form, useActionData, useLoaderData } from "react-router";
import { login } from "../../shopify.server";
import { loginErrorMessage } from "./error.server";
export const loader = async ({ request }) => {
const errors = loginErrorMessage(await login(request));
return { errors };
};
export const action = async ({ request }) => {
const errors = loginErrorMessage(await login(request));
return {
errors,
};
};
export default function Auth() {
const loaderData = useLoaderData();
const actionData = useActionData();
const [shop, setShop] = useState("");
const { errors } = actionData || loaderData;
return (
<AppProvider embedded={false}>
<s-page>
<Form method="post">
<s-section heading="Log in">
<s-text-field
name="shop"
label="Shop domain"
details="example.myshopify.com"
value={shop}
onChange={(e) => setShop(e.currentTarget.value)}
autocomplete="on"
error={errors.shop}
></s-text-field>
<s-button type="submit">Log in</s-button>
</s-section>
</Form>
</s-page>
</AppProvider>
);
}

View File

@ -0,0 +1,22 @@
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const action = async ({ request }) => {
const { payload, session, topic, shop } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
const current = payload.current;
if (session) {
await db.session.update({
where: {
id: session.id,
},
data: {
scope: current.toString(),
},
});
}
return new Response();
};

View File

@ -0,0 +1,16 @@
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const action = async ({ request }) => {
const { shop, session, topic } = await authenticate.webhook(request);
console.log(`Received ${topic} webhook for ${shop}`);
// Webhook requests can trigger multiple times and after an app has already been uninstalled.
// If this webhook already ran, the session may have been deleted previously.
if (session) {
await db.session.deleteMany({ where: { shop } });
}
return new Response();
};

34
app/shopify.server.js Normal file
View File

@ -0,0 +1,34 @@
import "@shopify/shopify-app-react-router/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
} from "@shopify/shopify-app-react-router/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
apiVersion: ApiVersion.October25,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL || "",
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
future: {
expiringOfflineAccessTokens: true,
},
...(process.env.SHOP_CUSTOM_DOMAIN
? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
: {}),
});
export default shopify;
export const apiVersion = ApiVersion.October25;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;

0
extensions/.gitkeep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,10 @@
{
"ratings": {
"stars": {
"label": "Ratings"
},
"home": {
"recommendationText": "Recommended Product!"
}
}
}

View File

@ -0,0 +1,3 @@
name = "new-theme"
type = "theme"
uid = "d9c67580-7562-42eb-8ad8-22d743afb81b"

12409
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

74
package.json Normal file
View File

@ -0,0 +1,74 @@
{
"name": "cartu",
"private": true,
"scripts": {
"build": "react-router build",
"dev": "shopify app dev",
"config:link": "shopify app config link",
"generate": "shopify app generate",
"deploy": "shopify app deploy",
"config:use": "shopify app config use",
"env": "shopify app env",
"start": "react-router-serve ./build/server/index.js",
"docker-start": "npm run setup && npm run start",
"setup": "prisma generate && prisma migrate deploy",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"shopify": "shopify",
"prisma": "prisma",
"graphql-codegen": "graphql-codegen",
"vite": "vite",
"typecheck": "react-router typegen && tsc --noEmit"
},
"type": "module",
"engines": {
"node": ">=20.19 <22 || >=22.12"
},
"dependencies": {
"@prisma/client": "^6.16.3",
"@react-router/dev": "^7.12.0",
"@react-router/fs-routes": "^7.12.0",
"@react-router/node": "^7.12.0",
"@react-router/serve": "^7.12.0",
"@shopify/app-bridge-react": "^4.2.4",
"@shopify/shopify-app-react-router": "^1.1.0",
"@shopify/shopify-app-session-storage-prisma": "^8.0.0",
"isbot": "^5.1.31",
"prisma": "^6.16.3",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.12.0",
"tiny-invariant": "^1.3.3",
"vite-tsconfig-paths": "^5.1.4"
},
"devDependencies": {
"@shopify/api-codegen-preset": "^1.2.0",
"@shopify/polaris-types": "^1.0.1",
"@types/eslint": "^9.6.1",
"@types/node": "^22.18.8",
"@types/react": "^18.3.25",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.10.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"graphql-config": "^5.1.1",
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vite": "^6.3.6"
},
"workspaces": [
"extensions/*"
],
"trustedDependencies": [
"@shopify/plugin-cloudflare"
],
"overrides": {
"p-map": "^4.0.0"
},
"author": "anshkumar"
}

View File

@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"shop" TEXT NOT NULL,
"state" TEXT NOT NULL,
"isOnline" BOOLEAN NOT NULL DEFAULT false,
"scope" TEXT,
"expires" DATETIME,
"accessToken" TEXT NOT NULL,
"userId" BIGINT,
"firstName" TEXT,
"lastName" TEXT,
"email" TEXT,
"accountOwner" BOOLEAN NOT NULL DEFAULT false,
"locale" TEXT,
"collaborator" BOOLEAN DEFAULT false,
"emailVerified" BOOLEAN DEFAULT false,
"refreshToken" TEXT,
"refreshTokenExpires" DATETIME
);

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

46
prisma/schema.prisma Normal file
View File

@ -0,0 +1,46 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
// enough when changing adapters.
// See https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string for more information
datasource db {
provider = "sqlite"
url = "file:dev.sqlite"
}
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
firstName String?
lastName String?
email String?
accountOwner Boolean @default(false)
locale String?
collaborator Boolean? @default(false)
emailVerified Boolean? @default(false)
refreshToken String?
refreshTokenExpires DateTime?
}
model QRCode {
id Int @id @default(autoincrement())
title String
shop String
productId String
productHandle String
productVariantId String
destination String
scans Int @default(0)
createdAt DateTime @default(now())
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

57
shopify.app.toml Normal file
View File

@ -0,0 +1,57 @@
# Learn more about configuring your app at https://shopify.dev/docs/apps/tools/cli/configuration
client_id = "8aacbed9f69521f6be048da82759895c"
name = "cartu"
application_url = "https://example.com"
embedded = true
[build]
automatically_update_urls_on_dev = true
include_config_on_deploy = true
[webhooks]
api_version = "2026-04"
[[webhooks.subscriptions]]
topics = [ "app/uninstalled" ]
uri = "/webhooks/app/uninstalled"
[[webhooks.subscriptions]]
topics = [ "app/scopes_update" ]
uri = "/webhooks/app/scopes_update"
[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "write_metaobject_definitions,write_metaobjects,write_products"
[auth]
redirect_urls = [ "https://example.com/api/auth" ]
[product.metafields.app.demo_info]
type = "single_line_text_field"
name = "Demo Source Info"
description = "Tracks products created by the Shopify app template for development"
[product.metafields.app.demo_info.access]
admin = "merchant_read_write"
[metaobjects.app.example]
name = "Example"
description = "An example metaobject definition created by this template"
[metaobjects.app.example.access]
admin = "merchant_read_write"
[metaobjects.app.example.fields.title]
name = "Title"
type = "single_line_text_field"
required = true
[metaobjects.app.example.fields.description]
name = "Description"
type = "multi_line_text_field"
[app_proxy]
url = "/app/proxy"
subpath = "hello"
prefix = "apps"

7
shopify.web.toml Normal file
View File

@ -0,0 +1,7 @@
name = "React Router"
roles = ["frontend", "backend"]
webhooks_path = "/webhooks/app/uninstalled"
[commands]
predev = "npx prisma generate"
dev = "npx prisma migrate deploy && npm exec react-router dev"

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"strict": true,
"skipLibCheck": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"removeComments": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"allowJs": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"baseUrl": ".",
"types": ["@react-router/node", "vite/client", "@shopify/polaris-types"],
"rootDirs": [".", "./.react-router/types"]
}
}

58
vite.config.js Normal file
View File

@ -0,0 +1,58 @@
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the Vite server.
// The CLI will eventually stop passing in HOST,
// so we can remove this workaround after the next major release.
if (
process.env.HOST &&
(!process.env.SHOPIFY_APP_URL ||
process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
process.env.SHOPIFY_APP_URL = process.env.HOST;
delete process.env.HOST;
}
const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
.hostname;
let hmrConfig;
if (host === "localhost") {
hmrConfig = {
protocol: "ws",
host: "localhost",
port: 64999,
clientPort: 64999,
};
} else {
hmrConfig = {
protocol: "wss",
host: host,
port: parseInt(process.env.FRONTEND_PORT) || 8002,
clientPort: 443,
};
}
export default defineConfig({
server: {
allowedHosts: [host],
cors: {
preflightContinue: true,
},
port: Number(process.env.PORT || 3000),
hmr: hmrConfig,
fs: {
// See https://vitejs.dev/config/server-options.html#server-fs-allow for more information
allow: ["app", "node_modules"],
},
},
plugins: [reactRouter(), tsconfigPaths()],
build: {
assetsInlineLimit: 0,
},
optimizeDeps: {
include: ["@shopify/app-bridge-react"],
},
});