├── .dockerignore ├── .github └── dependabot.yml ├── .gitignore ├── .npmrc ├── .vscode └── extensions.json ├── Dockerfile ├── README.md ├── SECURITY.md ├── package.json ├── shopify.app.toml └── web ├── frontend ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── @types │ └── window.d.ts ├── App.tsx ├── README.md ├── Routes.tsx ├── assets │ ├── empty-state.svg │ ├── home-trophy.png │ └── index.ts ├── components │ ├── ProductsCard.tsx │ ├── index.ts │ └── providers │ │ ├── AppBridgeProvider.tsx │ │ ├── PolarisProvider.tsx │ │ ├── QueryProvider.tsx │ │ └── index.ts ├── dev_embed.js ├── hooks │ ├── index.ts │ ├── useAppQuery.ts │ └── useAuthenticatedFetch.ts ├── index.html ├── index.tsx ├── package.json ├── pages │ ├── ExitIframe.tsx │ ├── NotFound.tsx │ ├── index.tsx │ └── pagename.tsx ├── shopify.web.toml ├── tsconfig.json └── vite.config.js ├── gdpr.ts ├── index.ts ├── nodemon.json ├── package.json ├── product-creator.ts ├── shopify.ts ├── shopify.web.toml └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | web/node_modules 2 | web/frontend/node_modules 3 | web/frontend/dist 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "npm" # See documentation for possible values 13 | directory: "/web" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "npm" 17 | directory: "/web/frontend" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment Configuration 2 | .env 3 | .env.* 4 | 5 | # Dependency directory 6 | node_modules 7 | 8 | # Test coverage directory 9 | coverage 10 | 11 | # Ignore Apple macOS Desktop Services Store 12 | .DS_Store 13 | 14 | # Logs 15 | logs 16 | *.log 17 | 18 | # ngrok tunnel file 19 | config/tunnel.pid 20 | 21 | # vite build output 22 | dist/ 23 | 24 | # extensions build output 25 | extensions/*/build 26 | 27 | # Node library SQLite database 28 | web/database.sqlite 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | auto-install-peers=true 3 | shamefully-hoist=true 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "shopify.polaris-for-vscode" 4 | ] 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | ARG SHOPIFY_API_KEY 4 | ENV SHOPIFY_API_KEY=$SHOPIFY_API_KEY 5 | EXPOSE 8081 6 | WORKDIR /app 7 | COPY web . 8 | RUN npm install 9 | RUN cd frontend && npm install && npm run build 10 | CMD ["npm", "run", "serve"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopify App Template - Node with Typescript 2 | 3 | This is a template for building a [Shopify app] using Node and React with Typescript. 4 | 5 | ## What is the differences from [original](https://github.com/Shopify/shopify-app-template-node)? 6 | 7 | - Almost js files converted to ts 8 | - Add some packages 9 | - Eslint 10 | - Prettier 11 | - Types for existing packages 12 | - Update run script(especially for backend) 13 | - Define original types(There are still a few any types left) 14 | 15 | **Please feel free to contribute if you find some issues.** 16 | 17 | # Shopify App Template - Node(Original) 18 | 19 | This is a template for building a [Shopify app](https://shopify.dev/apps/getting-started) using Node and React. It contains the basics for building a Shopify app. 20 | 21 | Rather than cloning this repo, you can use your preferred package manager and the Shopify CLI with [these steps](#installing-the-template). 22 | 23 | ## Benefits 24 | 25 | Shopify apps are built on a variety of Shopify tools to create a great merchant experience. The [create an app](https://shopify.dev/apps/getting-started/create) tutorial in our developer documentation will guide you through creating a Shopify app using this template. 26 | 27 | The Node app template comes with the following out-of-the-box functionality: 28 | 29 | - OAuth: Installing the app and granting permissions 30 | - GraphQL Admin API: Querying or mutating Shopify admin data 31 | - REST Admin API: Resource classes to interact with the API 32 | - Shopify-specific tooling: 33 | - AppBridge 34 | - Polaris 35 | - Webhooks 36 | 37 | ## Tech Stack 38 | 39 | This template combines a number of third party open-source tools: 40 | 41 | - [Express](https://expressjs.com/) builds the backend. 42 | - [Vite](https://vitejs.dev/) builds the [React](https://reactjs.org/) frontend. 43 | - [React Router](https://reactrouter.com/) is used for routing. We wrap this with file-based routing. 44 | - [React Query](https://react-query.tanstack.com/) queries the Admin API. 45 | 46 | The following Shopify tools complement these third-party tools to ease app development: 47 | 48 | - [Shopify API library](https://github.com/Shopify/shopify-node-api) adds OAuth to the Express backend. This lets users install the app and grant scope permissions. 49 | - [App Bridge React](https://shopify.dev/apps/tools/app-bridge/getting-started/using-react) adds authentication to API requests in the frontend and renders components outside of the App’s iFrame. 50 | - [Polaris React](https://polaris.shopify.com/) is a powerful design system and component library that helps developers build high quality, consistent experiences for Shopify merchants. 51 | - [Custom hooks](https://github.com/Shopify/shopify-frontend-template-react/tree/main/hooks) make authenticated requests to the Admin API. 52 | - [File-based routing](https://github.com/Shopify/shopify-frontend-template-react/blob/main/Routes.jsx) makes creating new pages easier. 53 | 54 | ## Getting started 55 | 56 | ### Requirements 57 | 58 | 1. You must [download and install Node.js](https://nodejs.org/en/download/) if you don't already have it. 59 | 1. You must [create a Shopify partner account](https://partners.shopify.com/signup) if you don’t have one. 60 | 1. You must [create a development store](https://help.shopify.com/en/partners/dashboard/development-stores#create-a-development-store) if you don’t have one. 61 | 62 | ### Installing the template 63 | 64 | This template can be installed using your preferred package manager: 65 | 66 | Using yarn: 67 | 68 | ```shell 69 | yarn create @shopify/app 70 | ``` 71 | 72 | Using npx: 73 | 74 | ```shell 75 | npm init @shopify/app@latest 76 | ``` 77 | 78 | Using pnpm: 79 | 80 | ```shell 81 | pnpm create @shopify/app@latest 82 | ``` 83 | 84 | This will clone the template and install the required dependencies. 85 | 86 | #### Local Development 87 | 88 | [The Shopify CLI](https://shopify.dev/apps/tools/cli) connects to an app in your Partners dashboard. It provides environment variables, runs commands in parallel, and updates application URLs for easier development. 89 | 90 | You can develop locally using your preferred package manager. Run one of the following commands from the root of your app. 91 | 92 | Using yarn: 93 | 94 | ```shell 95 | yarn dev 96 | ``` 97 | 98 | Using npm: 99 | 100 | ```shell 101 | npm run dev 102 | ``` 103 | 104 | Using pnpm: 105 | 106 | ```shell 107 | pnpm run dev 108 | ``` 109 | 110 | Open the URL generated in your console. Once you grant permission to the app, you can start development. 111 | 112 | ## Deployment 113 | 114 | ### Application Storage 115 | 116 | This template uses [SQLite](https://www.sqlite.org/index.html) to store session data. The database is a file called `database.sqlite` which is automatically created in the root. This use of SQLite works in production if your app runs as a single instance. 117 | 118 | The database that works best for you depends on the data your app needs and how it is queried. You can run your database of choice on a server yourself or host it with a SaaS company. Here’s a short list of databases providers that provide a free tier to get started: 119 | 120 | | Database | Type | Hosters | 121 | | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 122 | | MySQL | SQL | [Digital Ocean](https://www.digitalocean.com/try/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) | 123 | | PostgreSQL | SQL | [Digital Ocean](https://www.digitalocean.com/try/managed-databases-postgresql), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Google Cloud SQL](https://cloud.google.com/sql/docs/postgres) | 124 | | Redis | Key-value | [Digital Ocean](https://www.digitalocean.com/try/managed-databases-redis), [Amazon MemoryDB](https://aws.amazon.com/memorydb/) | 125 | | MongoDB | NoSQL / Document | [Digital Ocean](https://www.digitalocean.com/try/managed-databases-mongodb), [MongoDB Atlas](https://www.mongodb.com/atlas/database) | 126 | 127 | To use one of these, you need to change your session storage configuration. To help, here’s a list of [SessionStorage adapter packages](https://github.com/Shopify/shopify-api-js/tree/main/docs/usage/session-storage.md). 128 | 129 | ### Build 130 | 131 | The frontend is a single page app. It requires the `SHOPIFY_API_KEY`, which you can find on the page for your app in your partners dashboard. Paste your app’s key in the command for the package manager of your choice: 132 | 133 | Using yarn: 134 | 135 | ```shell 136 | cd web/frontend/ && SHOPIFY_API_KEY=REPLACE_ME yarn build 137 | ``` 138 | 139 | Using npm: 140 | 141 | ```shell 142 | cd web/frontend/ && SHOPIFY_API_KEY=REPLACE_ME npm run build 143 | ``` 144 | 145 | Using pnpm: 146 | 147 | ```shell 148 | cd web/frontend/ && SHOPIFY_API_KEY=REPLACE_ME pnpm run build 149 | ``` 150 | 151 | You do not need to build the backend. 152 | 153 | ## Hosting 154 | 155 | When you're ready to set up your app in production, you can follow [our deployment documentation](https://shopify.dev/apps/deployment/web) to host your app on a cloud provider like [Heroku](https://www.heroku.com/) or [Fly.io](https://fly.io/). 156 | 157 | When you reach the step for [setting up environment variables](https://shopify.dev/apps/deployment/web#set-env-vars), you also need to set the variable `NODE_ENV=production`. 158 | 159 | ## Known issues 160 | 161 | ### Hot module replacement and Firefox 162 | 163 | When running the app with the CLI in development mode on Firefox, you might see your app constantly reloading when you access it. 164 | That happened in previous versions of the CLI, because of the way HMR websocket requests work. 165 | 166 | We fixed this issue with v3.4.0 of the CLI, so after updating it, you can make the following changes to your app's `web/frontend/vite.config.js` file: 167 | 168 | 1. Change the definition `hmrConfig` object to be: 169 | 170 | ```js 171 | const host = process.env.HOST 172 | ? process.env.HOST.replace(/https?:\/\//, "") 173 | : "localhost"; 174 | 175 | let hmrConfig; 176 | if (host === "localhost") { 177 | hmrConfig = { 178 | protocol: "ws", 179 | host: "localhost", 180 | port: 64999, 181 | clientPort: 64999, 182 | }; 183 | } else { 184 | hmrConfig = { 185 | protocol: "wss", 186 | host: host, 187 | port: process.env.FRONTEND_PORT, 188 | clientPort: 443, 189 | }; 190 | } 191 | ``` 192 | 193 | 1. Change the `server.host` setting in the configs to `"localhost"`: 194 | 195 | ```js 196 | server: { 197 | host: "localhost", 198 | ... 199 | ``` 200 | 201 | ### I can't get past the ngrok "Visit site" page 202 | 203 | When you’re previewing your app or extension, you might see an ngrok interstitial page with a warning: 204 | 205 | ```text 206 | You are about to visit .ngrok.io: Visit Site 207 | ``` 208 | 209 | If you click the `Visit Site` button, but continue to see this page, then you should run dev using an alternate tunnel URL that you run using tunneling software. 210 | We've validated that [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare/) works with this template. 211 | 212 | To do that, you can [install the `cloudflared` CLI tool](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/), and run: 213 | 214 | ```shell 215 | # Note that you can also use a different port 216 | cloudflared tunnel --url http://localhost:3000 217 | ``` 218 | 219 | Out of the logs produced by cloudflare you will notice a https URL where the domain ends with `trycloudflare.com`. This is your tunnel URL. You need to copy this URL as you will need it in the next step. 220 | 221 | ```shell 222 | 2022-11-11T19:57:55Z INF Requesting new quick Tunnel on trycloudflare.com... 223 | 2022-11-11T19:57:58Z INF +--------------------------------------------------------------------------------------------+ 224 | 2022-11-11T19:57:58Z INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): | 225 | 2022-11-11T19:57:58Z INF | https://randomly-generated-hostname.trycloudflare.com | 226 | 2022-11-11T19:57:58Z INF +--------------------------------------------------------------------------------------------+ 227 | ``` 228 | 229 | Below you would replace `randomly-generated-hostname` with what you have copied from the terminal. In a different terminal window, navigate to your app's root and with the URL from above you would call: 230 | 231 | ```shell 232 | # Using yarn 233 | yarn dev --tunnel-url https://randomly-generated-hostname.trycloudflare.com:3000 234 | # or using npm 235 | npm run dev --tunnel-url https://randomly-generated-hostname.trycloudflare.com:3000 236 | # or using pnpm 237 | pnpm dev --tunnel-url https://randomly-generated-hostname.trycloudflare.com:3000 238 | ``` 239 | 240 | ## Developer resources 241 | 242 | - [Introduction to Shopify apps](https://shopify.dev/apps/getting-started) 243 | - [App authentication](https://shopify.dev/apps/auth) 244 | - [Shopify CLI](https://shopify.dev/apps/tools/cli) 245 | - [Shopify API Library documentation](https://github.com/Shopify/shopify-api-js#readme) 246 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported versions 4 | 5 | ### New features 6 | 7 | New features will only be added to the master branch and will not be made available in point releases. 8 | 9 | ### Bug fixes 10 | 11 | Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from. 12 | 13 | ### Security issues 14 | 15 | Only the latest release series will receive patches and new versions in case of a security issue. 16 | 17 | ### Severe security issues 18 | 19 | For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. 20 | 21 | ### Unsupported Release Series 22 | 23 | When a release series is no longer supported, it's your own responsibility to deal with bugs and security issues. If you are not comfortable maintaining your own versions, you should upgrade to a supported version. 24 | 25 | ## Reporting a bug 26 | 27 | All security bugs in shopify repositories should be reported to [our hackerone program](https://hackerone.com/shopify) 28 | Shopify's whitehat program is our way to reward security researchers for finding serious security vulnerabilities in the In Scope properties listed at the bottom of this page, including our core application (all functionality associated with a Shopify store, particularly your-store.myshopify.com/admin) and certain ancillary applications. 29 | 30 | ## Disclosure Policy 31 | 32 | We look forward to working with all security researchers and strive to be respectful, always assume the best and treat others as peers. We expect the same in return from all participants. To achieve this, our team strives to: 33 | 34 | - Reply to all reports within one business day and triage within two business days (if applicable) 35 | - Be as transparent as possible, answering all inquires about our report decisions and adding hackers to duplicate HackerOne reports 36 | - Award bounties within a week of resolution (excluding extenuating circumstances) 37 | - Only close reports as N/A when the issue reported is included in Known Issues, Ineligible Vulnerabilities Types or lacks evidence of a vulnerability 38 | 39 | **The following rules must be followed in order for any rewards to be paid:** 40 | 41 | - You may only test against shops you have created which include your HackerOne YOURHANDLE @ wearehackerone.com registered email address. 42 | - You must not attempt to gain access to, or interact with, any shops other than those created by you. 43 | - The use of commercial scanners is prohibited (e.g., Nessus). 44 | - Rules for reporting must be followed. 45 | - Do not disclose any issues publicly before they have been resolved. 46 | - Shopify reserves the right to modify the rules for this program or deem any submissions invalid at any time. Shopify may cancel the whitehat program without notice at any time. 47 | - Contacting Shopify Support over chat, email or phone about your HackerOne report is not allowed. We may disqualify you from receiving a reward, or from participating in the program altogether. 48 | - You are not an employee of Shopify; employees should report bugs to the internal bug bounty program. 49 | - You hereby represent, warrant and covenant that any content you submit to Shopify is an original work of authorship and that you are legally entitled to grant the rights and privileges conveyed by these terms. You further represent, warrant and covenant that the consent of no other person or entity is or will be necessary for Shopify to use the submitted content. 50 | - By submitting content to Shopify, you irrevocably waive all moral rights which you may have in the content. 51 | - All content submitted by you to Shopify under this program is licensed under the MIT License. 52 | - You must report any discovered vulnerability to Shopify as soon as you have validated the vulnerability. 53 | - Failure to follow any of the foregoing rules will disqualify you from participating in this program. 54 | 55 | \*\* Please see our [Hackerone Profile](https://hackerone.com/shopify) for full details 56 | 57 | ## Receiving Security Updates 58 | 59 | To receive all general updates to vulnerabilities, please subscribe to our hackerone [Hacktivity](https://hackerone.com/shopify/hacktivity) 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-typescript-template-node-typescript", 3 | "version": "1.0.0", 4 | "main": "web/index.js", 5 | "license": "UNLICENSED", 6 | "scripts": { 7 | "shopify": "shopify", 8 | "build": "shopify app build", 9 | "dev": "shopify app dev", 10 | "info": "shopify app info", 11 | "generate": "shopify app generate", 12 | "deploy": "shopify app deploy" 13 | }, 14 | "dependencies": { 15 | "@shopify/app": "3.54.0", 16 | "@shopify/cli": "3.54.0" 17 | }, 18 | "author": "Tomoya Nakano@tomotomy" 19 | } 20 | -------------------------------------------------------------------------------- /shopify.app.toml: -------------------------------------------------------------------------------- 1 | #This file stores configurations for your Shopify app. 2 | 3 | scopes = "write_products" 4 | -------------------------------------------------------------------------------- /web/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "overrides": [], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": "latest", 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["react", "@typescript-eslint"], 24 | "rules": { 25 | "react/react-in-jsx-scope": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Ignore Apple macOS Desktop Services Store 5 | .DS_Store 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # vite build output 12 | dist/ 13 | 14 | # Partners can use npm, yarn or pnpm with the CLI. 15 | # We ignore lock files so they don't get a package manager mis-match 16 | # Without this, they may get a warning if using a different package manager to us 17 | yarn.lock 18 | package-lock.json 19 | -------------------------------------------------------------------------------- /web/frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /web/frontend/@types/window.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | interface Window { 4 | __SHOPIFY_DEV_HOST: string 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /web/frontend/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter } from 'react-router-dom' 2 | import { NavigationMenu } from '@shopify/app-bridge-react' 3 | import Routes from './Routes' 4 | 5 | import { AppBridgeProvider, QueryProvider, PolarisProvider } from './components' 6 | 7 | export default function App() { 8 | // Any .tsx or .jsx files in /pages will become a route 9 | // See documentation for for more info 10 | const pages = import.meta.glob('./pages/**/!(*.test.[jt]sx)*.([jt]sx)', { 11 | eager: true, 12 | }) 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /web/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Shopify React Frontend App 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) 4 | 5 | This repository is the frontend for Shopify’s app starter templates. **You probably don’t want to use this repository directly**, but rather through one of the templates and the [Shopify CLI](https://github.com/Shopify/shopify-cli). 6 | 7 | ## Developer resources 8 | 9 | - [Introduction to Shopify apps](https://shopify.dev/apps/getting-started) 10 | - [App authentication](https://shopify.dev/apps/auth) 11 | - [Shopify CLI command reference](https://shopify.dev/apps/tools/cli/app) 12 | - [Shopify API Library documentation](https://github.com/Shopify/shopify-node-api/tree/main/docs) 13 | 14 | ## License 15 | 16 | This repository is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 17 | -------------------------------------------------------------------------------- /web/frontend/Routes.tsx: -------------------------------------------------------------------------------- 1 | import { Routes as ReactRouterRoutes, Route } from 'react-router-dom' 2 | 3 | /** 4 | * File-based routing. 5 | * @desc File-based routing that uses React Router under the hood. 6 | * To create a new route create a new .jsx file in `/pages` with a default export. 7 | * 8 | * Some examples: 9 | * * `/pages/index.jsx` matches `/` 10 | * * `/pages/blog/[id].jsx` matches `/blog/123` 11 | * * `/pages/[...catchAll].jsx` matches any URL not explicitly matched 12 | * 13 | * @param {object} pages value of import.meta.globEager(). See https://vitejs.dev/guide/features.html#glob-import 14 | * 15 | * @return {Routes} `` from React Router, with a `` for each file in `pages` 16 | */ 17 | export default function Routes({ pages }: any) { 18 | const routes = useRoutes(pages) 19 | const routeComponents = routes.map(({ path, component: Component }) => ( 20 | } /> 21 | )) 22 | 23 | const NotFound = routes.find(({ path }) => path === '/notFound').component 24 | 25 | return ( 26 | 27 | {routeComponents} 28 | } /> 29 | 30 | ) 31 | } 32 | 33 | function useRoutes(pages: any) { 34 | const routes = Object.keys(pages) 35 | .map((key) => { 36 | let path = key 37 | .replace('./pages', '') 38 | .replace(/\.(t|j)sx?$/, '') 39 | /** 40 | * Replace /index with / 41 | */ 42 | .replace(/\/index$/i, '/') 43 | /** 44 | * Only lowercase the first letter. This allows the developer to use camelCase 45 | * dynamic paths while ensuring their standard routes are normalized to lowercase. 46 | */ 47 | .replace(/\b[A-Z]/, (firstLetter) => firstLetter.toLowerCase()) 48 | /** 49 | * Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom 50 | */ 51 | .replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`) 52 | 53 | if (path.endsWith('/') && path !== '/') { 54 | path = path.substring(0, path.length - 1) 55 | } 56 | 57 | if (!pages[key].default) { 58 | console.warn(`${key} doesn't export a default React component`) 59 | } 60 | 61 | return { 62 | path, 63 | component: pages[key].default, 64 | } 65 | }) 66 | .filter((route) => route.component) 67 | 68 | return routes 69 | } 70 | -------------------------------------------------------------------------------- /web/frontend/assets/empty-state.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/frontend/assets/home-trophy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoyanakano/shopify-app-template-node-typescript/8e9a1b1bf1d0d39976d8045adf5cbb9df0440642/web/frontend/assets/home-trophy.png -------------------------------------------------------------------------------- /web/frontend/assets/index.ts: -------------------------------------------------------------------------------- 1 | export { default as notFoundImage } from './empty-state.svg' 2 | export { default as trophyImage } from './home-trophy.png' 3 | -------------------------------------------------------------------------------- /web/frontend/components/ProductsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { 3 | Card, 4 | Heading, 5 | TextContainer, 6 | DisplayText, 7 | TextStyle, 8 | } from '@shopify/polaris' 9 | import { Toast } from '@shopify/app-bridge-react' 10 | import { useAppQuery, useAuthenticatedFetch } from '../hooks' 11 | 12 | type EmptyToastProps = { 13 | content: string 14 | error?: boolean 15 | } 16 | 17 | export function ProductsCard() { 18 | const emptyToastProps: EmptyToastProps = { content: null } 19 | const [isLoading, setIsLoading] = useState(true) 20 | const [toastProps, setToastProps] = useState(emptyToastProps) 21 | const fetch = useAuthenticatedFetch() 22 | 23 | const { 24 | data, 25 | refetch: refetchProductCount, 26 | isLoading: isLoadingCount, 27 | isRefetching: isRefetchingCount, 28 | } = useAppQuery({ 29 | url: '/api/products/count', 30 | reactQueryOptions: { 31 | onSuccess: () => { 32 | setIsLoading(false) 33 | }, 34 | }, 35 | }) 36 | 37 | const toastMarkup = toastProps.content && !isRefetchingCount && ( 38 | setToastProps(emptyToastProps)} /> 39 | ) 40 | 41 | const handlePopulate = async () => { 42 | setIsLoading(true) 43 | const response = await fetch('/api/products/create') 44 | 45 | if (response.ok) { 46 | await refetchProductCount() 47 | setToastProps({ content: '5 products created!' }) 48 | } else { 49 | setIsLoading(false) 50 | setToastProps({ 51 | content: 'There was an error creating products', 52 | error: true, 53 | }) 54 | } 55 | } 56 | 57 | return ( 58 | <> 59 | {toastMarkup} 60 | 69 | 70 |

71 | Sample products are created with a default title and price. You can 72 | remove them at any time. 73 |

74 | 75 | TOTAL PRODUCTS 76 | 77 | 78 | {isLoadingCount ? '-' : data.count} 79 | 80 | 81 | 82 |
83 |
84 | 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /web/frontend/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ProductsCard } from './ProductsCard' 2 | export * from './providers' 3 | -------------------------------------------------------------------------------- /web/frontend/components/providers/AppBridgeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react' 2 | import { useLocation, useNavigate } from 'react-router-dom' 3 | import { Provider } from '@shopify/app-bridge-react' 4 | import { Banner, Layout, Page } from '@shopify/polaris' 5 | 6 | /** 7 | * A component to configure App Bridge. 8 | * @desc A thin wrapper around AppBridgeProvider that provides the following capabilities: 9 | * 10 | * 1. Ensures that navigating inside the app updates the host URL. 11 | * 2. Configures the App Bridge Provider, which unlocks functionality provided by the host. 12 | * 13 | * See: https://shopify.dev/apps/tools/app-bridge/react-components 14 | */ 15 | export function AppBridgeProvider({ children }: any) { 16 | const location = useLocation() 17 | const navigate = useNavigate() 18 | const history = useMemo( 19 | () => ({ 20 | replace: (path: any) => { 21 | navigate(path, { replace: true }) 22 | }, 23 | }), 24 | [navigate] 25 | ) 26 | 27 | const routerConfig = useMemo( 28 | () => ({ history, location }), 29 | [history, location] 30 | ) 31 | 32 | // The host may be present initially, but later removed by navigation. 33 | // By caching this in state, we ensure that the host is never lost. 34 | // During the lifecycle of an app, these values should never be updated anyway. 35 | // Using state in this way is preferable to useMemo. 36 | // See: https://stackoverflow.com/questions/60482318/version-of-usememo-for-caching-a-value-that-will-never-change 37 | const [appBridgeConfig] = useState(() => { 38 | const host = 39 | new URLSearchParams(location.search).get('host') || 40 | window.__SHOPIFY_DEV_HOST 41 | 42 | window.__SHOPIFY_DEV_HOST = host 43 | 44 | return { 45 | host, 46 | apiKey: process.env.SHOPIFY_API_KEY, 47 | forceRedirect: true, 48 | } 49 | }) 50 | 51 | if (!process.env.SHOPIFY_API_KEY || !appBridgeConfig.host) { 52 | const bannerProps = !process.env.SHOPIFY_API_KEY 53 | ? { 54 | title: 'Missing Shopify API Key', 55 | children: ( 56 | <> 57 | Your app is running without the SHOPIFY_API_KEY environment 58 | variable. Please ensure that it is set when running or building 59 | your React app. 60 | 61 | ), 62 | } 63 | : { 64 | title: 'Missing host query argument', 65 | children: ( 66 | <> 67 | Your app can only load if the URL has a host argument. 68 | Please ensure that it is set, or access your app using the 69 | Partners Dashboard Test your app feature 70 | 71 | ), 72 | } 73 | 74 | return ( 75 | 76 | 77 | 78 |
79 | 80 |
81 |
82 |
83 |
84 | ) 85 | } 86 | 87 | return ( 88 | 89 | {children} 90 | 91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /web/frontend/components/providers/PolarisProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { AppProvider } from '@shopify/polaris' 3 | import { useNavigate } from '@shopify/app-bridge-react' 4 | import translations from '@shopify/polaris/locales/en.json' 5 | import '@shopify/polaris/build/esm/styles.css' 6 | 7 | function AppBridgeLink({ url, children, external, ...rest }: any) { 8 | const navigate = useNavigate() 9 | const handleClick = useCallback(() => { 10 | navigate(url) 11 | }, [url]) 12 | 13 | const IS_EXTERNAL_LINK_REGEX = /^(?:[a-z][a-z\d+.-]*:|\/\/)/ 14 | 15 | if (external || IS_EXTERNAL_LINK_REGEX.test(url)) { 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ) 28 | } 29 | 30 | type Props = { 31 | children: React.ReactElement | React.ReactElement[] 32 | } 33 | 34 | /** 35 | * Sets up the AppProvider from Polaris. 36 | * @desc PolarisProvider passes a custom link component to Polaris. 37 | * The Link component handles navigation within an embedded app. 38 | * Prefer using this vs any other method such as an anchor. 39 | * Use it by importing Link from Polaris, e.g: 40 | * 41 | * ``` 42 | * import {Link} from '@shopify/polaris' 43 | * 44 | * function MyComponent() { 45 | * return ( 46 | *
Tab 2
47 | * ) 48 | * } 49 | * ``` 50 | * 51 | * PolarisProvider also passes translations to Polaris. 52 | * 53 | */ 54 | export function PolarisProvider({ children }: Props) { 55 | return ( 56 | 57 | {children} 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /web/frontend/components/providers/QueryProvider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | QueryClient, 3 | QueryClientProvider, 4 | QueryCache, 5 | MutationCache, 6 | } from 'react-query' 7 | 8 | type Props = { 9 | children: React.ReactElement | React.ReactElement[] 10 | } 11 | 12 | /** 13 | * Sets up the QueryClientProvider from react-query. 14 | * @desc See: https://react-query.tanstack.com/reference/QueryClientProvider#_top 15 | */ 16 | export function QueryProvider({ children }: Props) { 17 | const client = new QueryClient({ 18 | queryCache: new QueryCache(), 19 | mutationCache: new MutationCache(), 20 | }) 21 | 22 | return {children} 23 | } 24 | -------------------------------------------------------------------------------- /web/frontend/components/providers/index.ts: -------------------------------------------------------------------------------- 1 | export { AppBridgeProvider } from './AppBridgeProvider' 2 | export { QueryProvider } from './QueryProvider' 3 | export { PolarisProvider } from './PolarisProvider' 4 | -------------------------------------------------------------------------------- /web/frontend/dev_embed.js: -------------------------------------------------------------------------------- 1 | import RefreshRuntime from '/@react-refresh' 2 | 3 | RefreshRuntime.injectIntoGlobalHook(window) 4 | window.$RefreshReg$ = () => {} 5 | window.$RefreshSig$ = () => (type) => type 6 | window.__vite_plugin_react_preamble_installed__ = true 7 | -------------------------------------------------------------------------------- /web/frontend/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useAppQuery } from './useAppQuery' 2 | export { useAuthenticatedFetch } from './useAuthenticatedFetch' 3 | -------------------------------------------------------------------------------- /web/frontend/hooks/useAppQuery.ts: -------------------------------------------------------------------------------- 1 | import { useAuthenticatedFetch } from './useAuthenticatedFetch' 2 | import { useMemo } from 'react' 3 | import { useQuery } from 'react-query' 4 | 5 | /** 6 | * A hook for querying your custom app data. 7 | * @desc A thin wrapper around useAuthenticatedFetch and react-query's useQuery. 8 | * 9 | * @param {Object} options - The options for your query. Accepts 3 keys: 10 | * 11 | * 1. url: The URL to query. E.g: /api/widgets/1` 12 | * 2. fetchInit: The init options for fetch. See: https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters 13 | * 3. reactQueryOptions: The options for `useQuery`. See: https://react-query.tanstack.com/reference/useQuery 14 | * 15 | * @returns Return value of useQuery. See: https://react-query.tanstack.com/reference/useQuery. 16 | */ 17 | export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => { 18 | const authenticatedFetch = useAuthenticatedFetch() 19 | const fetch = useMemo(() => { 20 | return async () => { 21 | const response = await authenticatedFetch(url, fetchInit) 22 | return response.json() 23 | } 24 | }, [url, JSON.stringify(fetchInit)]) 25 | 26 | return useQuery(url, fetch, { 27 | ...reactQueryOptions, 28 | refetchOnWindowFocus: false, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /web/frontend/hooks/useAuthenticatedFetch.ts: -------------------------------------------------------------------------------- 1 | import { authenticatedFetch } from '@shopify/app-bridge/utilities' 2 | import { useAppBridge } from '@shopify/app-bridge-react' 3 | import { Redirect } from '@shopify/app-bridge/actions' 4 | import { AppBridgeState, ClientApplication } from '@shopify/app-bridge' 5 | 6 | /** 7 | * A hook that returns an auth-aware fetch function. 8 | * @desc The returned fetch function that matches the browser's fetch API 9 | * See: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 10 | * It will provide the following functionality: 11 | * 12 | * 1. Add a `X-Shopify-Access-Token` header to the request. 13 | * 2. Check response for `X-Shopify-API-Request-Failure-Reauthorize` header. 14 | * 3. Redirect the user to the reauthorization URL if the header is present. 15 | * 16 | * @returns {Function} fetch function 17 | */ 18 | export function useAuthenticatedFetch() { 19 | const app = useAppBridge() 20 | const fetchFunction = authenticatedFetch(app) 21 | 22 | return async (uri: RequestInfo, options?: RequestInit) => { 23 | const response = await fetchFunction(uri, options) 24 | checkHeadersForReauthorization(response.headers, app) 25 | return response 26 | } 27 | } 28 | 29 | function checkHeadersForReauthorization( 30 | headers: Headers, 31 | app: ClientApplication 32 | ) { 33 | if (headers.get('X-Shopify-API-Request-Failure-Reauthorize') === '1') { 34 | const authUrlHeader = 35 | headers.get('X-Shopify-API-Request-Failure-Reauthorize-Url') || 36 | `/api/auth` 37 | 38 | const redirect = Redirect.create(app) 39 | redirect.dispatch( 40 | Redirect.Action.REMOTE, 41 | authUrlHeader.startsWith('/') 42 | ? `https://${window.location.host}${authUrlHeader}` 43 | : authUrlHeader 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /web/frontend/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('app')) 6 | -------------------------------------------------------------------------------- /web/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-frontend-template-react", 3 | "private": true, 4 | "license": "UNLICENSED", 5 | "scripts": { 6 | "build": "vite build", 7 | "dev": "vite", 8 | "coverage": "vitest run --coverage", 9 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 10 | "lint:fix": " npm run lint -- --fix" 11 | }, 12 | "type": "module", 13 | "engines": { 14 | "node": ">= 12.16" 15 | }, 16 | "dependencies": { 17 | "@shopify/app-bridge": "^3.7.7", 18 | "@shopify/app-bridge-react": "^3.7.7", 19 | "@shopify/polaris": "^10.49.1", 20 | "@types/react-router-dom": "^5.3.3", 21 | "@vitejs/plugin-react": "4.2.1", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-query": "^3.34.19", 25 | "react-router-dom": "^6.3.0", 26 | "vite": "^5.0.12" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^20.4.2", 30 | "@typescript-eslint/eslint-plugin": "^7.0.0", 31 | "@typescript-eslint/parser": "^7.4.0", 32 | "eslint": "^8.30.0", 33 | "eslint-config-prettier": "^9.0.0", 34 | "eslint-plugin-prettier": "^5.0.0", 35 | "eslint-plugin-react": "^7.31.11", 36 | "history": "^5.3.0", 37 | "jsdom": "^24.0.0", 38 | "prettier": "^3.0.0", 39 | "typescript": "^5.1.6", 40 | "vi-fetch": "^0.8.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /web/frontend/pages/ExitIframe.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect } from '@shopify/app-bridge/actions' 2 | import { useAppBridge, Loading } from '@shopify/app-bridge-react' 3 | import { useEffect } from 'react' 4 | import { useLocation } from 'react-router-dom' 5 | 6 | export default function ExitIframe() { 7 | const app = useAppBridge() 8 | const { search } = useLocation() 9 | 10 | useEffect(() => { 11 | if (!!app && !!search) { 12 | const params = new URLSearchParams(search) 13 | const redirectUri = params.get('redirectUri') 14 | const url = new URL(decodeURIComponent(redirectUri)) 15 | 16 | if (url.hostname === location.hostname) { 17 | const redirect = Redirect.create(app) 18 | redirect.dispatch( 19 | Redirect.Action.REMOTE, 20 | decodeURIComponent(redirectUri) 21 | ) 22 | } 23 | } 24 | }, [app, search]) 25 | 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /web/frontend/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Card, EmptyState, Page } from '@shopify/polaris' 2 | import { notFoundImage } from '../assets' 3 | 4 | export default function NotFound() { 5 | return ( 6 | 7 | 8 | 9 | 13 |

14 | Check the URL and try again, or use the search bar to find what 15 | you need. 16 |

17 |
18 |
19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /web/frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Page, 4 | Layout, 5 | TextContainer, 6 | Image, 7 | Stack, 8 | Link, 9 | Heading, 10 | } from '@shopify/polaris' 11 | import { TitleBar } from '@shopify/app-bridge-react' 12 | 13 | import { trophyImage } from '../assets' 14 | 15 | import { ProductsCard } from '../components' 16 | 17 | export default function HomePage() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | Nice work on building a Shopify app 🎉 33 |

34 | Your app is ready to explore! It contains everything you 35 | need to get started including the{' '} 36 | 37 | Polaris design system 38 | 39 | ,{' '} 40 | 41 | Shopify Admin API 42 | 43 | , and{' '} 44 | 48 | App Bridge 49 | {' '} 50 | UI library and components. 51 |

52 |

53 | Ready to go? Start populating your app with some sample 54 | products to view and test in your store.{' '} 55 |

56 |

57 | Learn more about building out your app in{' '} 58 | 62 | this Shopify tutorial 63 | {' '} 64 | 📚{' '} 65 |

66 |
67 |
68 | 69 |
70 | Nice work on building a Shopify app 75 |
76 |
77 |
78 |
79 |
80 | 81 | 82 | 83 |
84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /web/frontend/pages/pagename.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Page, Layout, TextContainer, Heading } from '@shopify/polaris' 2 | import { TitleBar } from '@shopify/app-bridge-react' 3 | 4 | export default function PageName() { 5 | return ( 6 | 7 | console.log('Primary action'), 12 | }} 13 | secondaryActions={[ 14 | { 15 | content: 'Secondary action', 16 | onAction: () => console.log('Secondary action'), 17 | }, 18 | ]} 19 | /> 20 | 21 | 22 | 23 | Heading 24 | 25 |

Body

26 |
27 |
28 | 29 | Heading 30 | 31 |

Body

32 |
33 |
34 |
35 | 36 | 37 | Heading 38 | 39 |

Body

40 |
41 |
42 |
43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /web/frontend/shopify.web.toml: -------------------------------------------------------------------------------- 1 | type="frontend" 2 | 3 | [commands] 4 | dev = "npm run dev" 5 | build = "npm run build" 6 | -------------------------------------------------------------------------------- /web/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "allowJs": false, 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "esModuleInterop": true, 11 | "target": "esnext", 12 | "noImplicitAny": true, 13 | "moduleResolution": "node", 14 | "sourceMap": true, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "outDir": "dist", 18 | "baseUrl": ".", 19 | "noEmit": true, 20 | "paths": { 21 | "*": [ 22 | "node_modules/*" 23 | ] 24 | }, 25 | "resolveJsonModule": true, 26 | "isolatedModules": true, 27 | "jsx": "react-jsx", 28 | "types": ["vite/client", "node"], 29 | "typeRoots": ["./node_modules/@types", "./@types"] 30 | }, 31 | "include": [ 32 | "**/*.ts", 33 | "**/*.tsx" 34 | ], 35 | "exclude": [ 36 | "node_modules/**" 37 | ], 38 | } -------------------------------------------------------------------------------- /web/frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import https from 'https' 5 | import react from '@vitejs/plugin-react' 6 | 7 | if ( 8 | process.env.npm_lifecycle_event === 'build' && 9 | !process.env.CI && 10 | !process.env.SHOPIFY_API_KEY 11 | ) { 12 | console.warn( 13 | '\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' 14 | ) 15 | } 16 | 17 | const proxyOptions = { 18 | target: `http://127.0.0.1:${process.env.BACKEND_PORT}`, 19 | changeOrigin: false, 20 | secure: true, 21 | ws: false, 22 | } 23 | 24 | const host = process.env.HOST 25 | ? process.env.HOST.replace(/https?:\/\//, '') 26 | : 'localhost' 27 | 28 | let hmrConfig 29 | if (host === 'localhost') { 30 | hmrConfig = { 31 | protocol: 'ws', 32 | host: 'localhost', 33 | port: 64999, 34 | clientPort: 64999, 35 | } 36 | } else { 37 | hmrConfig = { 38 | protocol: 'wss', 39 | host: host, 40 | port: process.env.FRONTEND_PORT, 41 | clientPort: 443, 42 | } 43 | } 44 | 45 | export default defineConfig({ 46 | root: dirname(fileURLToPath(import.meta.url)), 47 | plugins: [react()], 48 | define: { 49 | 'process.env.SHOPIFY_API_KEY': JSON.stringify(process.env.SHOPIFY_API_KEY), 50 | }, 51 | resolve: { 52 | preserveSymlinks: true, 53 | }, 54 | server: { 55 | host: 'localhost', 56 | port: process.env.FRONTEND_PORT, 57 | hmr: hmrConfig, 58 | proxy: { 59 | '^/(\\?.*)?$': proxyOptions, 60 | '^/api(/|(\\?.*)?$)': proxyOptions, 61 | }, 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /web/gdpr.ts: -------------------------------------------------------------------------------- 1 | import { DeliveryMethod } from "@shopify/shopify-api"; 2 | import { WebhookHandlersParam } from "@shopify/shopify-app-express"; 3 | 4 | const GDPRWebhookHandlers: WebhookHandlersParam = { 5 | /** 6 | * Customers can request their data from a store owner. When this happens, 7 | * Shopify invokes this webhook. 8 | * 9 | * https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks#customers-data_request 10 | */ 11 | CUSTOMERS_DATA_REQUEST: { 12 | deliveryMethod: DeliveryMethod.Http, 13 | callbackUrl: "/api/webhooks", 14 | callback: async (topic: any, shop: any, body: any, webhookId: any) => { 15 | const payload = JSON.parse(body); 16 | // Payload has the following shape: 17 | // { 18 | // "shop_id": 954889, 19 | // "shop_domain": "{shop}.myshopify.com", 20 | // "orders_requested": [ 21 | // 299938, 22 | // 280263, 23 | // 220458 24 | // ], 25 | // "customer": { 26 | // "id": 191167, 27 | // "email": "john@example.com", 28 | // "phone": "555-625-1199" 29 | // }, 30 | // "data_request": { 31 | // "id": 9999 32 | // } 33 | // } 34 | }, 35 | }, 36 | 37 | /** 38 | * Store owners can request that data is deleted on behalf of a customer. When 39 | * this happens, Shopify invokes this webhook. 40 | * 41 | * https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks#customers-redact 42 | */ 43 | CUSTOMERS_REDACT: { 44 | deliveryMethod: DeliveryMethod.Http, 45 | callbackUrl: "/api/webhooks", 46 | callback: async (topic: any, shop: any, body: any, webhookId: any) => { 47 | const payload = JSON.parse(body); 48 | // Payload has the following shape: 49 | // { 50 | // "shop_id": 954889, 51 | // "shop_domain": "{shop}.myshopify.com", 52 | // "customer": { 53 | // "id": 191167, 54 | // "email": "john@example.com", 55 | // "phone": "555-625-1199" 56 | // }, 57 | // "orders_to_redact": [ 58 | // 299938, 59 | // 280263, 60 | // 220458 61 | // ] 62 | // } 63 | }, 64 | }, 65 | 66 | /** 67 | * 48 hours after a store owner uninstalls your app, Shopify invokes this 68 | * webhook. 69 | * 70 | * https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks#shop-redact 71 | */ 72 | SHOP_REDACT: { 73 | deliveryMethod: DeliveryMethod.Http, 74 | callbackUrl: "/api/webhooks", 75 | callback: async (topic: any, shop: any, body: any, webhookId: any) => { 76 | const payload = JSON.parse(body); 77 | // Payload has the following shape: 78 | // { 79 | // "shop_id": 954889, 80 | // "shop_domain": "{shop}.myshopify.com" 81 | // } 82 | }, 83 | }, 84 | }; 85 | 86 | export default GDPRWebhookHandlers; 87 | -------------------------------------------------------------------------------- /web/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { join } from "path"; 3 | import { readFileSync } from "fs"; 4 | import express from "express"; 5 | import serveStatic from "serve-static"; 6 | 7 | import shopify from "./shopify.js"; 8 | import productCreator from "./product-creator.js"; 9 | import GDPRWebhookHandlers from "./gdpr.js"; 10 | 11 | const PORT = parseInt(process.env.BACKEND_PORT || process.env.PORT || "", 10); 12 | 13 | const STATIC_PATH = 14 | process.env.NODE_ENV === "production" 15 | ? `${process.cwd()}/frontend/dist` 16 | : `${process.cwd()}/frontend/`; 17 | 18 | const app = express(); 19 | 20 | // Set up Shopify authentication and webhook handling 21 | app.get(shopify.config.auth.path, shopify.auth.begin()); 22 | app.get( 23 | shopify.config.auth.callbackPath, 24 | shopify.auth.callback(), 25 | shopify.redirectToShopifyOrAppRoot() 26 | ); 27 | app.post( 28 | shopify.config.webhooks.path, 29 | shopify.processWebhooks({ webhookHandlers: GDPRWebhookHandlers }) 30 | ); 31 | 32 | // All endpoints after this point will require an active session 33 | app.use("/api/*", shopify.validateAuthenticatedSession()); 34 | 35 | app.use(express.json()); 36 | 37 | app.get("/api/products/count", async (_req, res) => { 38 | const countData = await shopify.api.rest.Product.count({ 39 | session: res.locals.shopify.session, 40 | }); 41 | res.status(200).send(countData); 42 | }); 43 | 44 | app.get("/api/products/create", async (_req, res) => { 45 | let status = 200; 46 | let error = null; 47 | 48 | try { 49 | await productCreator(res.locals.shopify.session); 50 | } catch (e: any) { 51 | console.log(`Failed to process products/create: ${e.message}`); 52 | status = 500; 53 | error = e.message; 54 | } 55 | res.status(status).send({ success: status === 200, error }); 56 | }); 57 | 58 | app.use(serveStatic(STATIC_PATH, { index: false })); 59 | 60 | app.use("/*", shopify.ensureInstalledOnShop(), async (_req, res, _next) => { 61 | return res 62 | .status(200) 63 | .set("Content-Type", "text/html") 64 | .send(readFileSync(join(STATIC_PATH, "index.html"))); 65 | }); 66 | 67 | app.listen(PORT); 68 | -------------------------------------------------------------------------------- /web/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["./"], 3 | "ignore": ["./frontend"], 4 | "ext": "ts,js,json", 5 | "exec": "node --loader ts-node/esm" 6 | } 7 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-app-template-node", 3 | "private": true, 4 | "license": "UNLICENSED", 5 | "scripts": { 6 | "debug": "node --inspect-brk index.js", 7 | "dev": "cross-env NODE_ENV=development nodemon index.ts", 8 | "serve": "cross-env NODE_ENV=production node index.ts" 9 | }, 10 | "type": "module", 11 | "engines": { 12 | "node": ">=14.13.1" 13 | }, 14 | "dependencies": { 15 | "@shopify/shopify-api": "^9.0.2", 16 | "@shopify/shopify-app-express": "^4.0.0", 17 | "@shopify/shopify-app-session-storage-sqlite": "^2.0.4", 18 | "compression": "^1.7.4", 19 | "cross-env": "^7.0.3", 20 | "serve-static": "^1.14.1" 21 | }, 22 | "devDependencies": { 23 | "@types/compression": "^1.7.2", 24 | "@types/express": "^4.17.15", 25 | "@types/node": "^20.4.2", 26 | "@types/nodemon": "^1.19.2", 27 | "@types/serve-static": "^1.15.0", 28 | "@typescript-eslint/eslint-plugin": "^7.0.0", 29 | "@typescript-eslint/parser": "^7.4.0", 30 | "eslint": "^8.30.0", 31 | "eslint-config-prettier": "^9.0.0", 32 | "eslint-plugin-react": "^7.31.11", 33 | "jsonwebtoken": "^9.0.1", 34 | "nodemon": "^3.0.1", 35 | "prettier": "^3.0.0", 36 | "pretty-quick": "^4.0.0", 37 | "ts-node": "^10.9.1", 38 | "typescript": "^5.1.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/product-creator.ts: -------------------------------------------------------------------------------- 1 | import { GraphqlQueryError } from "@shopify/shopify-api"; 2 | import shopify from "./shopify.js"; 3 | 4 | const ADJECTIVES = [ 5 | "autumn", 6 | "hidden", 7 | "bitter", 8 | "misty", 9 | "silent", 10 | "empty", 11 | "dry", 12 | "dark", 13 | "summer", 14 | "icy", 15 | "delicate", 16 | "quiet", 17 | "white", 18 | "cool", 19 | "spring", 20 | "winter", 21 | "patient", 22 | "twilight", 23 | "dawn", 24 | "crimson", 25 | "wispy", 26 | "weathered", 27 | "blue", 28 | "billowing", 29 | "broken", 30 | "cold", 31 | "damp", 32 | "falling", 33 | "frosty", 34 | "green", 35 | "long", 36 | ]; 37 | 38 | const NOUNS = [ 39 | "waterfall", 40 | "river", 41 | "breeze", 42 | "moon", 43 | "rain", 44 | "wind", 45 | "sea", 46 | "morning", 47 | "snow", 48 | "lake", 49 | "sunset", 50 | "pine", 51 | "shadow", 52 | "leaf", 53 | "dawn", 54 | "glitter", 55 | "forest", 56 | "hill", 57 | "cloud", 58 | "meadow", 59 | "sun", 60 | "glade", 61 | "bird", 62 | "brook", 63 | "butterfly", 64 | "bush", 65 | "dew", 66 | "dust", 67 | "field", 68 | "fire", 69 | "flower", 70 | ]; 71 | 72 | export const DEFAULT_PRODUCTS_COUNT = 5; 73 | const CREATE_PRODUCTS_MUTATION = ` 74 | mutation populateProduct($input: ProductInput!) { 75 | productCreate(input: $input) { 76 | product { 77 | id 78 | } 79 | } 80 | } 81 | `; 82 | 83 | export default async function productCreator( 84 | session: any, 85 | count = DEFAULT_PRODUCTS_COUNT 86 | ) { 87 | const client = new shopify.api.clients.Graphql({ session }); 88 | 89 | try { 90 | for (let i = 0; i < count; i++) { 91 | await client.request(CREATE_PRODUCTS_MUTATION, { 92 | variables: { 93 | input: { 94 | title: `${randomTitle()}`, 95 | variants: [{ price: randomPrice() }], 96 | }, 97 | }, 98 | }); 99 | } 100 | } catch (error: any) { 101 | if (error instanceof GraphqlQueryError) { 102 | throw new Error( 103 | `${error.message}\n${JSON.stringify(error.response, null, 2)}` 104 | ); 105 | } else { 106 | throw error; 107 | } 108 | } 109 | } 110 | 111 | function randomTitle() { 112 | const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; 113 | const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; 114 | return `${adjective} ${noun}`; 115 | } 116 | 117 | function randomPrice() { 118 | return Math.round((Math.random() * 10 + Number.EPSILON) * 100) / 100; 119 | } 120 | -------------------------------------------------------------------------------- /web/shopify.ts: -------------------------------------------------------------------------------- 1 | import { BillingInterval, LATEST_API_VERSION } from "@shopify/shopify-api"; 2 | import { shopifyApp } from "@shopify/shopify-app-express"; 3 | import { SQLiteSessionStorage } from "@shopify/shopify-app-session-storage-sqlite"; 4 | let { restResources } = await import( 5 | `@shopify/shopify-api/rest/admin/${LATEST_API_VERSION}` 6 | ); 7 | // If you want IntelliSense for the rest resources, you should import them directly 8 | // import { restResources } from "@shopify/shopify-api/rest/admin/2022-10"; 9 | 10 | const DB_PATH = `${process.cwd()}/database.sqlite`; 11 | 12 | // The transactions with Shopify will always be marked as test transactions, unless NODE_ENV is production. 13 | // See the ensureBilling helper to learn more about billing in this template. 14 | const billingConfig = { 15 | "My Shopify One-Time Charge": { 16 | // This is an example configuration that would do a one-time charge for $5 (only USD is currently supported) 17 | amount: 5.0, 18 | currencyCode: "USD", 19 | interval: BillingInterval.OneTime, 20 | }, 21 | }; 22 | 23 | const shopify = shopifyApp({ 24 | api: { 25 | apiVersion: LATEST_API_VERSION, 26 | restResources, 27 | billing: undefined, // or replace with billingConfig above to enable example billing 28 | }, 29 | auth: { 30 | path: "/api/auth", 31 | callbackPath: "/api/auth/callback", 32 | }, 33 | webhooks: { 34 | path: "/api/webhooks", 35 | }, 36 | // This should be replaced with your preferred storage strategy 37 | sessionStorage: new SQLiteSessionStorage(DB_PATH), 38 | }); 39 | 40 | export default shopify; 41 | -------------------------------------------------------------------------------- /web/shopify.web.toml: -------------------------------------------------------------------------------- 1 | type="backend" 2 | 3 | [commands] 4 | dev = "npm run dev" 5 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------