;
7 | }
8 | }
9 |
10 | return <>{children}>;
11 | };
12 |
13 | export default AppBridgeProvider;
14 |
--------------------------------------------------------------------------------
/nextjs/docs/EXTENSIONS.md:
--------------------------------------------------------------------------------
1 | # Extensions
2 |
3 | Setting up extensions requires a shake up of the folder structure.
4 |
5 | - Create a new folder called `app/` that includes everything except for `docs/` and `.git/` folders.
6 |
7 | - You might have to enable viewing hidden files in your system.
8 | - macOS: Command + Shift + .
9 | - Linux: Control + H
10 | - Windows: https://kinsta.com/blog/show-hidden-files
11 |
12 | - Create a new folder called `extension/` in your root.
13 |
14 | - Ensure `app/` and `extension/` are in the same level.
15 |
16 | - Create a new `package.json` file by using `npm init --y` or manually creating a new file named `package.json` in your root and `extension/`.
17 |
18 | - Head into `app/package.json` and update `update:config` script to `node _developer/tomlWriter.js && cd .. && shopify app deploy`
19 |
20 | - I've tested it to work with `bun` and `npm` or other package managers may cause issue. If CLI doesn't detect your extensions, you can either switch to `bun` or change the `update:config` script to only `node _developer/tomlWriter.js` and run `shopify app deploy` from the root.
21 | - More information on this is [available here](https://github.com/kinngh/shopify-nextjs-prisma-app/discussions/53)
22 |
23 | - Now in your `extension/package.json`, it's great to have this script so it's easier to create new extensions, which will put your extension in `extension/extensions/extension-name`:
24 |
25 | ```javascript
26 | "generate": "shopify app generate extension"
27 | ```
28 |
29 | ## Notes
30 |
31 | For visual reference, this is what we're expecting the folder structure to look like:
32 |
33 | Simplified:
34 |
35 | 
36 |
37 | Detailed:
38 |
39 | 
40 |
41 | The `npm` vs `bun` difference once you update the script:
42 | 
43 |
--------------------------------------------------------------------------------
/nextjs/docs/NOTES.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | ## APIs
4 |
5 | - All your APIs need to use middlewares. For App Proxy, it's `export default withMiddleware("verifyProxy")(handler)` and for Apps route (the regular backend) it's `export default withMiddleware("verifyRequest")(handler);`.
6 | - Example implementations are available in `pages/api/proxy_route/json.js` and `pages/api/apps/index.js`
7 | - If you don't use the middlewares your app will be rejected and it's insecure to run APIs without context.
8 |
9 | ## Webhooks
10 |
11 | - Processing webhooks has changed a bit, if you're coming from the Mongo repo available [here](https://github.com/kinngh/shopify-node-express-mongodb-app).
12 | - To add your webhooks, head over to `utils/shopify.js` and at the bottom add all your webhooks and handlers.
13 | - I recommend offloading webhooks handling (except `APP_UNINSTALLED`) to a different service like Google PubSub, AWS EventBridge or Cloudflare Workers so if you're building at scale, you can handle the insane amounts of webhooks that you could be processing, during times like Black Friday when your app is put to test.
14 |
15 | ## Next.js
16 |
17 | - If you're unsure about what gets shipped to browser, check out [Next.js Code Elimination Tool](https://next-code-elimination.vercel.app)
18 |
--------------------------------------------------------------------------------
/nextjs/docs/SETUP.md:
--------------------------------------------------------------------------------
1 | # Setup
2 |
3 | This is an in-depth guide on using this repo. This goes over getting the base repo up and running, to understand how to add your own customizations server side like registering webhooks, routes, etc, refer to [Notes](/docs/NOTES.md).
4 |
5 | - [ ] Run `npm run g:install` to install global dependencies.
6 |
7 | - This isn't required to be run every single time, but is necessary for local development - Installing the Shopify/app and Shopify/cli packages. Please see `package.json` for more info.
8 |
9 | - [ ] Run `npm i --force` to install dependencies.
10 |
11 | - Substantial efforts have gone into ensuring we're using the latest package versions, and some incompatibility issues always pop up while installing. There are no negative effects on the functionality just yet, but if you find anything please open an issue.
12 |
13 | - [ ] Create a new app (Public or Custom) from your [Shopify Partner Dashboard](https://partners.shopify.com).
14 |
15 | - [ ] Build your `.env` file based on `.env.example`.
16 |
17 | - `SHOPIFY_API_KEY`: App API key.
18 | - `SHOPIFY_API_SECRET`: App secret.
19 | - `SHOPIFY_API_SCOPES`: Scopes required by your Shopify app. A list of access scopes can be found [here](https://shopify.dev/api/usage/access-scopes)
20 | - `SHOPIFY_APP_URL`: URL generated from Ngrok.
21 | - `SHOPIFY_API_VERSION`: Pre-filled to the latest version. All the calls in the repo are based off this API version so if you're downgrading please refer to the official docs instead. The repo is always kept up to date with the newest practices so you can rely on the basic repo to almost always work without depriciation errors popping up.
22 | - `DATABASE_URL`: Database connection URL. Since we're using Prisma ORM with this repo, it supports SQL and noSQL databases. Read more about it [here](https://www.prisma.io/stack)
23 | - `ENCRYPTION_STRING`: String to use for Cryption for encrypting sessions token. Add a random salt (or a random string of letters and numbers) and save it. If you loose the string you cannot decrypt your sessions and must be kept safely.
24 | - `APP_NAME`: Name of your app, as you've entered in Partner Dashboard.
25 | - `APP_HANDLE`: The URL handle of your app.
26 | - `APP_PROXY_PREFIX`: The prefix for your App Proxy's path, can be one of these:
27 | - apps
28 | - a
29 | - community
30 | - tools
31 | - `APP_PROXY_SUBPATH`: Subpath for your app proxy.
32 | - Leave `APP_PROXY_PREFIX` or `APP_PROXY_SUBPATH` blank and no App Proxy entries are created.
33 | - `POS_EMBEDDED`: Boolean. If your app is embedded in Shopify Point of Sale.
34 |
35 | - [ ] NPM Scripts
36 |
37 | - `dev`: Run in dev mode
38 | - `build`: Build for production
39 | - `start`: Start in production mode. Requires `npm run build` before starting.
40 | - `pretty`: Run `prettier` on the entire project.
41 | - `update`: Force updates all packages to latest version and requires you to manually run `npm i --force` after. Not recommended if you don't know what you're doing.
42 | -
43 | - `ngrok:auth`: Replace `` with your ngrok token and run it to activate ngrok.
44 | - `ngrok`: Starts ngrok on port 3000.
45 | - `cloudflare`: Starts cloudflare tunnel on port 3000 (make sure you have `cloudflared` installed).
46 | -
47 | - `g:install`: Required global installs for buildling Shopify apps.
48 | - `shopify`: Run `shopify` commands
49 | - `update:config`: [Managed Installation] Use the Shopify CLI to update your configuration. Auto writes your `toml` file to root and `extension/` for syncing.
50 | - `update:url`: [OAuth Installation] Use `@shopify/cli-kit` to update URLs to your Shopify partner dashboard. Requires a proper setup of `.env` file.
51 | -
52 | - `pg:create`: Create a new folder `database` and init a PostgreSQL instance. Requires you to have postgres installed.
53 | - Run `brew install postgresql`
54 | - `pg:start`: Start a PostgreSQL instance on `database`. Requires you to run `npm run pg:create` before you can do this.
55 | - `pg:stop`: Stop PostgreSQL server.
56 | -
57 | - `prisma`: Generic command to access `prisma` commands.
58 | - `prisma:push`: Push `schema.prisma` to your `DATABASE_URL` database.
59 | - `prisma:pull`: Pull database schema from `DATABASE_URL` database and generates a `schema.prisma` file.
60 | -
61 | - `prepare`: Reserved script to generate `@prisma/client`.
62 |
63 | - [ ] Setup Partner Dashboard
64 |
65 | - Run `npm run ngrok` or `npm run cloudflare` to generate your subdomain. Copy the `https://` domain and add it in `SHOPIFY_APP_URL` in your `.env` file.
66 | - Run `npm run update:config` to create and update your `shopify.app.toml` file and sync with Shopify.
67 | - GPDR handlers are available at `page/api/gdpr/` and the URLs that are auto registered via your toml are are:
68 | - Customers Data Request: `https:///api/gdpr/customers_data_request`
69 | - Customers Redact: `https:///api/gdpr/customers_redact`
70 | - Shop Redact: `https:///api/gdpr/shop_redact`
71 | - App Proxy routes are setup to allow accessing data from your app directly from the store. An example proxy route has been setup and is available at `/pages/api/proxy_route`. First you need to setup your base urls. Here's how to get it working:
72 |
73 | - Subpath Prefix: `apps` [fill in env]
74 | - Subpath: `next-proxy` [fill in env]
75 | - Proxy URL: `https:///api/proxy_route` [auto filled by `_developer/tomlWriter.js`]
76 |
77 | - So when a merchant visits `https://shop-url.com/apps/next-proxy/`, the response to that request will come from `https:///api/proxy_route`. A middleware has already been setup to check signatures so you don't have to worry about authenticating proxy calls, and is available at `utils/middleware/verifyProxy.js`.
78 | - Subsequently, any child requests will be mapped the same way. A call to `https://shop-url.com/apps/next-proxy/json` will be routed to `https:///api/proxy_route/json`.
79 | - To confirm if you've setup app proxy properly, head over to `https://shop-url.myshopify.com/apps/next-proxy/json` to confirm if you get a JSON being returned with the configuration set above^
80 | - A common _gotcha_ is if you're creating multiple apps that all use the same subpath (`next-proxy` in this case), all susbequent installs will throw a `404` error because Shopify serializes routes based on installation. To avoid this, please change the subpath to something that's unique to your app. I prefer using the format `<>-proxy`
81 |
82 | - [ ] Running App
83 |
84 | - If it's your first time connecting to said database, run `npx prisma db push` to get your database working.
85 | - Run `npm run dev`, your database and ngrok/cloudflare.
86 | - Install the app by heading over to `https://storename.myshopify.com/admin/oauth/install?client_id=SHOPIFY_API_KEY`.
87 |
88 | - [ ] Setting up extensions
89 | - See [Extensions](./EXTENSIONS.md)
90 |
--------------------------------------------------------------------------------
/nextjs/docs/SNIPPETS.md:
--------------------------------------------------------------------------------
1 | # Snippets
2 |
3 | A collection of snippets to quickly create further boilerplate code with relevant functions and checks.
4 |
5 | I selected these snippets from my regular use and code that I was writing over and over again. Instead of again including a `__templates/` directory that was causing a lot of confusion, it made more sense to add this as a workspace snippet library instead. This also avoided the issue of creating and maintaining extensions.
6 |
7 | ## Snippets
8 |
9 | | Snippet | Description |
10 | | ------------------------ | ----------------------------------------------- |
11 | | `sfc` | Create an arrow function component |
12 | | `createNewPage` | Create a new Polaris page with a Card |
13 | | `createapi` | Create a new endpoint for `/api` |
14 | | `createproxy` | Create a new endpoint for `/api/proxy_route` |
15 | | `createwebhook` | Create a new webhook function |
16 | | `createOnlineClientGql` | Create a new GraphQL Client with online tokens |
17 | | `createOfflineClientGql` | Create a new GraphQL Client with offline tokens |
18 |
--------------------------------------------------------------------------------
/nextjs/docs/migration/app-bridge-cdn.md:
--------------------------------------------------------------------------------
1 | # App Bridge CDN Migration Guide
2 |
3 | Moving from App Bridge React to CDN is pretty straight forward and not as dauting of a task.
4 |
5 | 1. Uninstall `@shopify/app-bridge` and `@shopify/app-bridge-react` packages. This is being replaced by import AppBridge from Shopify's CDN in `pages/_document.js` file.
6 | 2. `` is now `...` that takes a series of `...` or for Next.js specifically, `...` tags.
7 | 3. `AppBridgeProvider` is now just a check to see if the `shop` exists. We do this to stop the base URL of our app be accessible without a shop.
8 | 4. There's no need to use `useNavigate()` or `Redirect.app` anymore. Using `open()` works as expected. You can see examples of this sprinkled throughout the debug cards and exitframe.
9 | 5. Using the new Resource Picker can come as a bit of a challenge. A great way to use it is to encapsulate the resource picker in an async funtion and instead of returning values, update your state variables to account in for the change. I'll be adding in an example for this in the future.
10 |
11 | ## Misc upgrades
12 |
13 | 1. `useFetch` hook got an update to be more reliable. We're now passing different headers that don't rely on Shopify/AppBridge to handle, making it more reliable. Your use of `useFetch` hook doesn't change, just drop in the new `useFetch` hook and you're good to go.
14 | 2. `/exitframe/[...shop].js` has reliability changes. You might want to update the components in there for better clarity on reauthorization process.
15 | 3. Legacy Polaris elements are replaced with newer ones.
16 | 4. `verifyRequest` now checks for scope changes too. In dev mode, you might want to kill your dev server and restart for env to properly take effect, or you'll be stuck in an endless auth loop.
17 | 5. `pages/api/auth` have new changes. It's meant to be a drop in replacement so just replace your existing files with the new ones, and add in your changes.
18 | 6. Debug cards have been renamed and refreshed for better clarity and examples.
19 |
--------------------------------------------------------------------------------
/nextjs/docs/migration/clientProvider.md:
--------------------------------------------------------------------------------
1 | # Client Provider
2 |
3 | The `clientProvider` abstraction has gotten a makeover to simplify the API. Instead of exposting `graphqlClient` and `restClient` functions, it now has a namespace that contains both `online` and `offline` objects. Each object contains a `graphqlClient` and `restClient` function that can be used to create a client for the respective access mode.
4 |
5 | ## Usage
6 |
7 | ### Online Client
8 |
9 | ```javascript
10 | import clientProvider from "@/utils/clientProvider";
11 |
12 | const { client, shop, session } = await clientProvider.online.graphqlClient({
13 | req,
14 | res,
15 | });
16 |
17 | const { client, shop, session } = await clientProvider.online.restClient({
18 | req,
19 | res,
20 | });
21 | ```
22 |
23 | ### Offline Client
24 |
25 | ```javascript
26 | import clientProvider from "@/utils/clientProvider";
27 |
28 | const { client, shop, session } = await clientProvider.offline.graphqlClient({
29 | shop: req.user_shop,
30 | });
31 |
32 | const { client, shop, session } = await clientProvider.offline.restClient({
33 | shop: req.user_shop,
34 | });
35 | ```
36 |
37 | ## Steps
38 |
39 | 1. Head into `verifyRequest` and add in `req.user_shop = session.shop` after `req.user_session = session;`.
40 | 2. Update your `graphqlClient` and `restClient` calls to `clientProvider.online.graphqlClient` / `clientProvider.offline.graphqlClient` and `clientProvider.online.restClient` / `clientProvider.offline.restClient`
41 |
--------------------------------------------------------------------------------
/nextjs/docs/migration/managed-webhooks.md:
--------------------------------------------------------------------------------
1 | # Managed Webhooks
2 |
3 | We (finally) have the ability to decide what webhooks we want to recieve and what to exclude. This has some great implications, the major one being less compute resources being used so your servers cost less.
4 |
5 | ## Migration
6 |
7 | ### Files
8 |
9 | While all of this is going in a single commit, here's the entire log on what's changed and the reasoning behind it:
10 |
11 | - `isInitialLoad.js`
12 | We no longer need the `webhookRegistrar` function to register webhooks. So now that we're not making that GraphQL call to register webhooks, the initial load times of the app is much faster, helping with overall LCP.
13 |
14 | - `[...webhookTopic].js`
15 | This is an autogenrated file now. It creates a `switch/case` statement to handle all your webhook landings. This won't contain a case if you're using an external HTTP server, AWS EventBridge or Google PubSub, since the `switch/case` only handles requests that come to this server.
16 |
17 | - `shopify.js`
18 | Instead of using the `addHandlers()` function, we're now passing it as `shopify.user.webhooks`, which doesn't affect how the baseline works and this isn't that big of a change, but how you declare webhook topics have changed.
19 |
20 | - `tomlWriter.js` and `webhookWriter.js`
21 | This is the secret sauce of parsing files and creating TOMLs and other configuration. I highly suggest to not mess with this.
22 |
23 | ### GraphQL to TOML
24 |
25 | - Managed webhooks don't show up when you make the GQL call to get active webhooks, so the webhooks Debug card is now useless.
26 | - If you're migrating your live project from `webhookRegistrar()` / GraphQL based webhooks to managed webhooks, you need to manually remove the older webhooks, or both the webhooks are going to fire twice.
27 |
28 | ## Gotchas
29 |
30 | - Filter `:` is actually `=` and not a fuzzy search, unlike the rest of theShopify Search API it's following
31 | - Scope errors requires multiple pushes sometimes. Comment the webhooks in `shopify.js`, run `bun run update:config` to push your access scopes, uncomment the webhooks and run `bun run update:config` again and it'll work as expected.
32 |
--------------------------------------------------------------------------------
/nextjs/docs/migration/oauth-to-managed-installation.md:
--------------------------------------------------------------------------------
1 | # OAuth to Managed Installation
2 |
3 | Shopify introduced `Shopify Managed Installation` to get rid of screen flickering during auth, unnecessary auth redirects while fetching online sessions and other misc issues. To oversimplify, you pass the access tokens to Shopify and get Session tokens in return like a regular fetch and save it in your database. Here's a rundown of what's changed:
4 |
5 | 1. Changes in Auth
6 |
7 | The older way of doing auth is still supported from Shopify but for embedded apps in this repo, it's gone and so are the files. I've completely removed the older strait to run auth.
8 |
9 | - `pages/api/auth/index.js`: Removed.
10 | - `pages/api/auth/token.js`: Removed.
11 | - `pages/api/auth/callback.js`: Removed.
12 |
13 | 2. Updates to `isShopAvailable` function
14 |
15 | `pages/index.jsx` now calls a new function, `isInitialLoad` instead of `isShopAvailable`. The new strait means on the first load we get `id_token` as a query param that is exchanged for online and offline session tokens. `isInitialLoad` checks if these params exist, exchanges them for online and offline tokens and saves them in the db. Note we do not await the `sessionHandler.storeSession()` function here because that's eating up time on initial load and we don't want the first time to take longer than ~3 seconds.
16 |
17 | A new check also happens here, `isFreshInstall`. Since the database structure is kept the same to ensure smooth transition to the new auth, we can now check if the install was a fresh one. If the store doesn't exist in the `store` model, it's a new install, but if it does have a `Bool` value, that means it's either already installed or is a reinstall. While I've merged these in an if condition, you can break them apart and run your own checks if required.
18 |
19 | ```javascript
20 | if (!isFreshInstall || isFreshInstall?.isActive === false) {
21 | // !isFreshInstall -> New Install
22 | // isFreshInstall?.isActive === false -> Reinstall
23 | await freshInstall({ shop: onlineSession.shop });
24 | }
25 | ```
26 |
27 | This is now followed up with a `props` return since `getServerSideProps` has to return it.
28 |
29 | 3. Changes to `verifyRequest` and `ExitFrame`
30 |
31 | The `verifyRequest()` middleware now works completely differently. First we check for `authorization` headers in each `fetch()` since App Bridge CDN automatically adds headers to each `fetch`. Then a JWT validation is run to ensure the headers are valid, followed by getting the session id and rading from the database, check for expiry and fetch new tokens if the online tokens have expired. Then pass the session to use in subsequent routes as `req.user_session` and the middleware is done.
32 |
33 | A great thing about this is `ExitFrame` doesn't exist anymore. If the tokens are invalid, we throw a `401` and if the tokens are expired, we fetch them and move on to the next set.
34 |
35 | 4. Quick auth URL
36 |
37 | The quick auth URL has gotten an update. We've moved from `https://appurl.com/api/auth?shop=storename.myshopify.com` to `https://storename.myshopify.com/admin/oauth/install?client_id=SHOPIFY_API_KEY`, which now takes the merchant to the install screen.
38 |
39 | 5. Depricating `useFetch()` hook
40 |
41 | The idea of `useFetch()` was to redirect towards `ExitFrame` if the tokens had expired or not found - this is not required anymore. All vanilla `fetch` requests work since AppBridge CDN adds in authorization headers in the background.
42 |
43 | 6. Thoughts
44 |
45 | Managed installation is great. No flickering, no running through ExitFrame, it's 10/10 all around. The only problem is now you don't get a hit when someone comes over to the permissions screen and are only made aware of the store when the permissions are approved. The new `tomlWriter` was built so that you are still only relying on your `env` and that's writing your `shopify.app.toml` file to root (and `extension/` folder). It took a second to wrap my head around but once you get the hang of it, it's great.
46 |
--------------------------------------------------------------------------------
/nextjs/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": ["./*"],
6 | "@/utils": ["./utils/*"],
7 | "@/components": ["./components/*"],
8 | "@/hooks": ["./components/hooks/*"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/nextjs/middleware.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Middleware to add Content Security Policy headers to matched requests.
3 | */
4 |
5 | import { NextResponse } from "next/server";
6 |
7 | export const config = {
8 | matcher: [
9 | /*
10 | * Exceptions:
11 | * /api/auth, /api/webhooks, /api/proxy_route, /api/gdpr, /_next,
12 | * /_proxy, /_auth, /_static, /_vercel, /public (/favicon.ico, etc)
13 | */
14 | "/((?!api/auth|api/webhooks|api/proxy_route|api/gdpr|_next|_proxy|_auth|_static|_vercel|[\\w-]+\\.\\w+).*)",
15 | ],
16 | };
17 |
18 | /**
19 | * @param {NextRequest} request - The incoming request object.
20 | * @returns {NextResponse} The response object with modified headers.
21 | */
22 | export function middleware(request) {
23 | const {
24 | nextUrl: { search },
25 | } = request;
26 |
27 | /**
28 | * Convert the query string into an object.
29 | * @type {URLSearchParams}
30 | */
31 | const urlSearchParams = new URLSearchParams(search);
32 | const params = Object.fromEntries(urlSearchParams.entries());
33 |
34 | const shop = params.shop || "*.myshopify.com";
35 |
36 | /**
37 | * Construct the Next.js response and set the Content-Security-Policy header.
38 | * @type {NextResponse}
39 | */
40 | const res = NextResponse.next();
41 | res.headers.set(
42 | "Content-Security-Policy",
43 | `frame-ancestors https://${shop} https://admin.shopify.com;`
44 | );
45 |
46 | return res;
47 | }
48 |
--------------------------------------------------------------------------------
/nextjs/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | import "@shopify/shopify-api/adapters/node";
4 | import setupCheck from "./utils/setupCheck.js";
5 |
6 | setupCheck();
7 |
8 | console.log(`--> Running in ${process.env.NODE_ENV} mode`);
9 |
10 | const nextConfig = {
11 | reactStrictMode: true,
12 | env: {
13 | CONFIG_SHOPIFY_API_KEY: process.env.SHOPIFY_API_KEY,
14 | CONFIG_SHOPIFY_APP_URL: process.env.SHOPIFY_APP_URL,
15 | },
16 | };
17 |
18 | export default nextConfig;
19 |
--------------------------------------------------------------------------------
/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shopify-nextjs-prisma-app",
3 | "version": "2024.08.31",
4 | "type": "module",
5 | "author": {
6 | "name": "Harshdeep Singh Hura",
7 | "url": "https://harshdeephura.com"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/kinngh/shopify-nextjs-prisma-app.git"
12 | },
13 | "scripts": {
14 | "dev": "next dev",
15 | "build": "next build",
16 | "start": "next start",
17 | "pretty": "prettier --write ./",
18 | "update": "ncu -u",
19 | "-----> Tunnel <-----": "",
20 | "ngrok:auth": "ngrok authtoken ",
21 | "ngrok": "ngrok http 3000",
22 | "cloudflare": "cloudflared tunnel --url localhost:3000",
23 | "-----> Shopify <-----": "",
24 | "g:install": "npm i -g @shopify/app@latest @shopify/cli@latest",
25 | "shopify": "shopify",
26 | "update:config": "node _developer/tomlWriter.js && shopify app deploy; npm run pretty",
27 | "update:url": "node _developer/updateDashboard.js",
28 | "-----> Database <-----": "",
29 | "pg:create": "mkdir database; pg_ctl -D database init",
30 | "pg:start": "pg_ctl -D database start",
31 | "pg:stop": "pg_ctl -D database stop",
32 | "-----> Prisma <-----": "",
33 | "prisma": "npx prisma",
34 | "prisma:push": "npx prisma db push",
35 | "prisma:pull": "npx prisma db pull",
36 | "-----> Reserved Scripts <-----": "",
37 | "prepare": "npx prisma generate"
38 | },
39 | "dependencies": {
40 | "@prisma/client": "^5.20.0",
41 | "@shopify/polaris": "^13.9.0",
42 | "@shopify/shopify-api": "^11.4.1",
43 | "cryptr": "^6.3.0",
44 | "next": "14.2.13",
45 | "next-api-middleware": "^3.0.0",
46 | "react": "18.3.1",
47 | "react-dom": "18.3.1"
48 | },
49 | "devDependencies": {
50 | "@iarna/toml": "^2.2.5",
51 | "@shopify/cli-kit": "^3.67.2",
52 | "dotenv": "^16.4.5",
53 | "ngrok": "^5.0.0-beta.2",
54 | "npm-check-updates": "^17.1.3",
55 | "prettier": "^3.3.3",
56 | "prisma": "^5.20.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/nextjs/pages/_app.js:
--------------------------------------------------------------------------------
1 | import AppBridgeProvider from "@/components/providers/AppBridgeProvider";
2 | import { AppProvider as PolarisProvider } from "@shopify/polaris";
3 | import "@shopify/polaris/build/esm/styles.css";
4 | import translations from "@shopify/polaris/locales/en.json";
5 | import Link from "next/link";
6 |
7 | const App = ({ Component, pageProps }) => {
8 | return (
9 | <>
10 |
11 |
12 |
13 | Debug Cards
14 | Info
15 |
16 |
17 |
18 |
19 | >
20 | );
21 | };
22 |
23 | export default App;
24 |
--------------------------------------------------------------------------------
/nextjs/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/nextjs/pages/api/apps/debug/activeWebhooks.js:
--------------------------------------------------------------------------------
1 | import clientProvider from "@/utils/clientProvider";
2 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
3 |
4 | /**
5 | * @param {import("next").NextApiRequest} req - The HTTP request object.
6 | * @param {import("next").NextApiResponse} res - The HTTP response object.
7 | */
8 | const handler = async (req, res) => {
9 | if (req.method === "GET") {
10 | try {
11 | const { client } = await clientProvider.online.graphqlClient({
12 | req,
13 | res,
14 | });
15 | const activeWebhooks = await client.request(
16 | `{
17 | webhookSubscriptions(first: 25) {
18 | edges {
19 | node {
20 | topic
21 | endpoint {
22 | __typename
23 | ... on WebhookHttpEndpoint {
24 | callbackUrl
25 | }
26 | }
27 | }
28 | }
29 | }
30 | }`
31 | );
32 | return res.status(200).send(activeWebhooks);
33 | } catch (e) {
34 | console.error(`---> An error occured`, e);
35 | return res.status(400).send({ text: "Bad request" });
36 | }
37 | } else {
38 | res.status(400).send({ text: "Bad request" });
39 | }
40 | };
41 |
42 | export default withMiddleware("verifyRequest")(handler);
43 |
--------------------------------------------------------------------------------
/nextjs/pages/api/apps/debug/createNewSubscription.js:
--------------------------------------------------------------------------------
1 | import clientProvider from "@/utils/clientProvider";
2 | import withMiddleware from "@/utils/middleware/withMiddleware";
3 |
4 | /**
5 | * @param {import("next").NextApiRequest} req - The HTTP request object.
6 | * @param {import("next").NextApiResponse} res - The HTTP response object.
7 | */
8 | const handler = async (req, res) => {
9 | //false for offline session, true for online session
10 | const { client } = await clientProvider.online.graphqlClient({
11 | req,
12 | res,
13 | });
14 | const returnUrl = `${process.env.SHOPIFY_APP_URL}/?shop=${req.user_shop}`;
15 |
16 | const planName = "$10.25 plan";
17 | const planPrice = 10.25; //Always a decimal
18 |
19 | const response = await client.request(
20 | `mutation CreateSubscription{
21 | appSubscriptionCreate(
22 | name: "${planName}"
23 | returnUrl: "${returnUrl}"
24 | test: true
25 | lineItems: [
26 | {
27 | plan: {
28 | appRecurringPricingDetails: {
29 | price: { amount: ${planPrice}, currencyCode: USD }
30 | }
31 | }
32 | }
33 | ]
34 | ) {
35 | userErrors {
36 | field
37 | message
38 | }
39 | confirmationUrl
40 | appSubscription {
41 | id
42 | status
43 | }
44 | }
45 | }
46 | `
47 | );
48 |
49 | if (response.data.appSubscriptionCreate.userErrors.length > 0) {
50 | console.log(
51 | `--> Error subscribing ${req.user_shop} to plan:`,
52 | response.data.appSubscriptionCreate.userErrors
53 | );
54 | res.status(400).send({ error: "An error occured." });
55 | return;
56 | }
57 |
58 | res.status(200).send({
59 | confirmationUrl: `${response.data.appSubscriptionCreate.confirmationUrl}`,
60 | });
61 | return;
62 | };
63 |
64 | export default withMiddleware("verifyRequest")(handler);
65 |
--------------------------------------------------------------------------------
/nextjs/pages/api/apps/debug/getActiveSubscriptions.js:
--------------------------------------------------------------------------------
1 | import clientProvider from "@/utils/clientProvider";
2 | import withMiddleware from "@/utils/middleware/withMiddleware";
3 |
4 | /**
5 | * @param {import("next").NextApiRequest} req - The HTTP request object.
6 | * @param {import("next").NextApiResponse} res - The HTTP response object.
7 | */
8 | const handler = async (req, res) => {
9 | //false for offline session, true for online session
10 | const { client } = await clientProvider.online.graphqlClient({
11 | req,
12 | res,
13 | });
14 |
15 | const response = await client.request(
16 | `{
17 | appInstallation {
18 | activeSubscriptions {
19 | name
20 | status
21 | lineItems {
22 | plan {
23 | pricingDetails {
24 | ... on AppRecurringPricing {
25 | __typename
26 | price {
27 | amount
28 | currencyCode
29 | }
30 | interval
31 | }
32 | }
33 | }
34 | }
35 | test
36 | }
37 | }
38 | }`
39 | );
40 |
41 | res.status(200).send(response);
42 | };
43 |
44 | export default withMiddleware("verifyRequest")(handler);
45 |
--------------------------------------------------------------------------------
/nextjs/pages/api/apps/debug/gql.js:
--------------------------------------------------------------------------------
1 | import clientProvider from "@/utils/clientProvider";
2 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
3 |
4 | /**
5 | * @param {import("next").NextApiRequest} req - The HTTP request object.
6 | * @param {import("next").NextApiResponse} res - The HTTP response object.
7 | */
8 | const handler = async (req, res) => {
9 | if (req.method === "GET") {
10 | try {
11 | const { client } = await clientProvider.online.graphqlClient({
12 | req,
13 | res,
14 | });
15 | const shop = await client.request(`{shop{name}}`);
16 | return res.status(200).send({ text: shop.data.shop.name });
17 | } catch (e) {
18 | console.error(`---> An error occured`, e);
19 | return res.status(400).send({ text: "Bad request" });
20 | }
21 | } else {
22 | res.status(400).send({ text: "Bad request" });
23 | }
24 | };
25 |
26 | export default withMiddleware("verifyRequest")(handler);
27 |
--------------------------------------------------------------------------------
/nextjs/pages/api/apps/debug/index.js:
--------------------------------------------------------------------------------
1 | //This is the same as `pages/api/index.js`.
2 |
3 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
4 |
5 | /**
6 | * @param {import("next").NextApiRequest} req - The HTTP request object.
7 | * @param {import("next").NextApiResponse} res - The HTTP response object.
8 | */
9 | const handler = async (req, res) => {
10 | if (req.method === "GET") {
11 | return res
12 | .status(200)
13 | .send({ text: "This text is coming from `/api/apps route`" });
14 | }
15 |
16 | if (req.method === "POST") {
17 | return res.status(200).send(req.body);
18 | }
19 |
20 | return res.status(400).send({ text: "Bad request" });
21 | };
22 |
23 | export default withMiddleware("verifyRequest")(handler);
24 |
--------------------------------------------------------------------------------
/nextjs/pages/api/apps/index.js:
--------------------------------------------------------------------------------
1 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
2 |
3 | /**
4 | * @param {import("next").NextApiRequest} req - The HTTP request object.
5 | * @param {import("next").NextApiResponse} res - The HTTP response object.
6 | */
7 | const handler = async (req, res) => {
8 | if (req.method === "GET") {
9 | return res
10 | .status(200)
11 | .send({ text: "This text is coming from `/api/apps route`" });
12 | }
13 |
14 | if (req.method === "POST") {
15 | return res.status(200).send({ text: req.body.content });
16 | }
17 |
18 | return res.status(400).send({ text: "Bad request" });
19 | };
20 |
21 | export default withMiddleware("verifyRequest")(handler);
22 |
--------------------------------------------------------------------------------
/nextjs/pages/api/gdpr/customers_data_request.js:
--------------------------------------------------------------------------------
1 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
2 |
3 | /**
4 | * @param {import("next").NextApiRequest} req - The HTTP request object.
5 | * @param {import("next").NextApiResponse} res - The HTTP response object.
6 | */
7 | const handler = async (req, res) => {
8 | if (req.method !== "POST") {
9 | return res.status(401).send("Must be POST");
10 | }
11 | try {
12 | const { body } = req;
13 | const shop = req.body.shop_domain;
14 | console.log("gdpr/customers_data_request", body, shop);
15 | return res.status(200).send({ message: "ok" });
16 | } catch (e) {
17 | console.error(
18 | `---> An error occured at /api/gdpr/customers_data_request: ${e.message}`,
19 | e
20 | );
21 | return res.status(500).send({ error: true });
22 | }
23 | };
24 |
25 | export default withMiddleware("verifyHmac")(handler);
26 |
--------------------------------------------------------------------------------
/nextjs/pages/api/gdpr/customers_redact.js:
--------------------------------------------------------------------------------
1 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
2 |
3 | /**
4 | * @param {import("next").NextApiRequest} req - The HTTP request object.
5 | * @param {import("next").NextApiResponse} res - The HTTP response object.
6 | */
7 | const handler = async (req, res) => {
8 | if (req.method !== "POST") {
9 | return res.status(401).send("Must be POST");
10 | }
11 | try {
12 | const { body } = req;
13 | const shop = req.body.shop_domain;
14 | console.log("gdpr/customers_redact", body, shop);
15 | return res.status(200).send({ message: "ok" });
16 | } catch (e) {
17 | console.error(
18 | `---> An error occured at /api/gdpr/customers_redact: ${e.message}`,
19 | e
20 | );
21 | return res.status(500).send({ error: true });
22 | }
23 | };
24 |
25 | export default withMiddleware("verifyHmac")(handler);
26 |
--------------------------------------------------------------------------------
/nextjs/pages/api/gdpr/shop_redact.js:
--------------------------------------------------------------------------------
1 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
2 |
3 | /**
4 | * @param {import("next").NextApiRequest} req - The HTTP request object.
5 | * @param {import("next").NextApiResponse} res - The HTTP response object.
6 | */
7 | const handler = async (req, res) => {
8 | if (req.method !== "POST") {
9 | return res.status(401).send("Must be POST");
10 | }
11 | try {
12 | const { body } = req;
13 | const shop = req.body.shop_domain;
14 | console.log("gdpr/shop_redact", body, shop);
15 | return res.status(200).send({ message: "ok" });
16 | } catch (e) {
17 | console.error(
18 | `---> An error occured at /api/gdpr/shop_redact: ${e.message}`,
19 | e
20 | );
21 | return res.status(500).send({ error: true });
22 | }
23 | };
24 |
25 | export default withMiddleware("verifyHmac")(handler);
26 |
--------------------------------------------------------------------------------
/nextjs/pages/api/graphql.js:
--------------------------------------------------------------------------------
1 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
2 | import shopify from "@/utils/shopify.js";
3 | import sessionHandler from "@/utils/sessionHandler.js";
4 |
5 | /**
6 | * @param {import("next").NextApiRequest} req - The HTTP request object.
7 | * @param {import("next").NextApiResponse} res - The HTTP response object.
8 | */
9 | const handler = async (req, res) => {
10 | //Reject anything that's not a POST
11 | if (req.method !== "POST") {
12 | return res.status(400).send({ text: "We don't do that here." });
13 | }
14 |
15 | try {
16 | const sessionId = await shopify.session.getCurrentId({
17 | isOnline: true,
18 | rawRequest: req,
19 | rawResponse: res,
20 | });
21 | const session = await sessionHandler.loadSession(sessionId);
22 | const response = await shopify.clients.graphqlProxy({
23 | session,
24 | rawBody: req.body,
25 | });
26 |
27 | res.status(200).send(response.body);
28 | } catch (e) {
29 | console.error("An error occured at /api/graphql", e);
30 | return res.status(403).send(e);
31 | }
32 | };
33 |
34 | withMiddleware("verifyRequest")(handler);
35 |
--------------------------------------------------------------------------------
/nextjs/pages/api/index.js:
--------------------------------------------------------------------------------
1 | //TEMP
2 | import clientProvider from "@/utils/clientProvider";
3 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
4 |
5 | /**
6 | * @param {import("next").NextApiRequest} req - The HTTP request object.
7 | * @param {import("next").NextApiResponse} res - The HTTP response object.
8 | */
9 | const handler = async (req, res) => {
10 | if (req.method === "GET") {
11 | try {
12 | const { client } = await clientProvider.online.graphqlClient({
13 | req,
14 | res,
15 | });
16 | const activeWebhooks = await client.request(
17 | `{
18 | webhookSubscriptions(first: 25) {
19 | edges {
20 | node {
21 | topic
22 | endpoint {
23 | __typename
24 | ... on WebhookHttpEndpoint {
25 | callbackUrl
26 | }
27 | }
28 | }
29 | }
30 | }
31 | }`
32 | );
33 | return res.status(200).send(activeWebhooks);
34 | } catch (e) {
35 | console.error(`---> An error occured`, e);
36 | return res.status(400).send({ text: "Bad request" });
37 | }
38 | } else {
39 | res.status(400).send({ text: "Bad request" });
40 | }
41 | };
42 |
43 | export default withMiddleware("verifyRequest")(handler);
44 |
--------------------------------------------------------------------------------
/nextjs/pages/api/proxy_route/json.js:
--------------------------------------------------------------------------------
1 | import clientProvider from "@/utils/clientProvider";
2 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
3 |
4 | /**
5 | * @param {import("next").NextApiRequest} req - The HTTP request object.
6 | * @param {import("next").NextApiResponse} res - The HTTP response object.
7 | */
8 | const handler = async (req, res) => {
9 | const { client } = await clientProvider.offline.graphqlClient({
10 | shop: req.user_shop,
11 | });
12 |
13 | return res.status(200).send({ content: "Proxy Be Working" });
14 | };
15 |
16 | export default withMiddleware("verifyProxy")(handler);
17 |
--------------------------------------------------------------------------------
/nextjs/pages/api/proxy_route/wishlist/add_product.js:
--------------------------------------------------------------------------------
1 | import withMiddleware from "@/utils/middleware/withMiddleware.js";
2 | import prisma from "@/utils/prisma";
3 |
4 | const handler = async (req, res) => {
5 | if (req.method !== "POST") {
6 | //GET, POST, PUT, DELETE
7 | console.log("Serve this request only if method type is POST");
8 | return res.status(405).send({ error: true });
9 | }
10 | try {
11 | const reqbody = req.body;
12 |
13 | //Find wishlist for customers
14 | const wishlistsForCustomer = await prisma.customer.findMany({
15 | where: {
16 | id: req.customer_id,
17 | },
18 | select: {
19 | wishlists: true,
20 | },
21 | });
22 |
23 | //If wishlists are found, see if that wishlist id is valid
24 | if (wishlistsForCustomer.length > 0) {
25 | let wishlistToUpdate = reqbody.wishlist_id ? reqbody.wishlist_id : null;
26 |
27 | wishlistsForCustomer.map((wishlist) => {
28 | wishlistToUpdate = wishlist.wishlists.find((list) =>
29 | wishlistToUpdate
30 | ? JSON.stringify(list?.id) === JSON.stringify(wishlistToUpdate)
31 | : list.isDefault === true
32 | );
33 | });
34 |
35 | //TODO:- Rewrite this to make it simpler
36 | // What's happening: If it's default, it gives an object, if it's not default, it gives the id as string
37 | wishlistToUpdate?.id
38 | ? (wishlistToUpdate = wishlistToUpdate?.id)
39 | : wishlistToUpdate;
40 |
41 | if (!wishlistToUpdate) {
42 | return res.status(404).send({ error: "No wishlist found" });
43 | } else {
44 | const updateDefaultWishlist = await prisma.wishlists.update({
45 | where: {
46 | id: wishlistToUpdate,
47 | },
48 | data: {
49 | wishlist_product: {
50 | connectOrCreate: {
51 | where: {
52 | product_id_variant_id: {
53 | product_id: reqbody.product_id,
54 | variant_id: reqbody.variant_id,
55 | },
56 | },
57 | create: {
58 | product_id: reqbody.product_id,
59 | variant_id: reqbody.variant_id,
60 | title: reqbody.title,
61 | variant_title: reqbody.variant_title,
62 | },
63 | },
64 | },
65 | },
66 | });
67 | }
68 | } else {
69 | //Else create a new default wishlist
70 | const createDefaultWishlistForCustomer = await prisma.customer.create({
71 | data: {
72 | id: req.customer_id,
73 | name: "Not Logged In",
74 | email: "Not Logged In",
75 | wishlists: {
76 | create: {
77 | name: "Default Wishlist",
78 | isDefault: true,
79 | wishlist_product: {
80 | create: {
81 | product_id: reqbody.product_id,
82 | variant_id: reqbody.variant_id,
83 | title: reqbody.title,
84 | variant_title: reqbody.variant_title,
85 | },
86 | },
87 | },
88 | },
89 | },
90 | });
91 | }
92 |
93 | return res.status(200).send({ content: "Prcoxy Be Working" });
94 | } catch (e) {
95 | console.error(e);
96 | return res.status(403).send({ error: true });
97 | }
98 | };
99 |
100 | export default withMiddleware("verifyProxy")(handler);
101 |
--------------------------------------------------------------------------------
/nextjs/pages/api/webhooks/[...webhookTopic].js:
--------------------------------------------------------------------------------
1 | /**
2 | * DO NOT EDIT THIS FILE DIRECTLY
3 | * Head over to utils/shopify.js to create your webhooks
4 | * and write your webhook functions in utils/webhooks.
5 | * If you don't know the format, use the `createwebhook` snippet when using VSCode
6 | * to get a boilerplate function for webhooks.
7 | * To update this file, run `npm run update:config` or `bun run update:config`
8 | */
9 |
10 | import ordersHandler from "@/utils/webhooks/orders.js";
11 | import shopify from "@/utils/shopify.js";
12 | import appUninstallHandler from "@/utils/webhooks/app_uninstalled.js";
13 |
14 | async function buffer(readable) {
15 | const chunks = [];
16 | for await (const chunk of readable) {
17 | chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
18 | }
19 | return Buffer.concat(chunks);
20 | }
21 |
22 | export default async function handler(req, res) {
23 | if (req.method !== "POST") {
24 | return res.status(400).send("It ain't POST mate.");
25 | }
26 |
27 | const topic = req.headers["x-shopify-topic"] || "";
28 | const shop = req.headers["x-shopify-shop-domain"] || "";
29 | const apiVersion = req.headers["x-shopify-api-version"] || "";
30 | const webhookId = req.headers["x-shopify-webhook-id"] || "";
31 |
32 | const buff = await buffer(req);
33 | const rawBody = buff.toString("utf8");
34 |
35 | try {
36 | const validateWebhook = await shopify.webhooks.validate({
37 | rawBody: rawBody,
38 | rawRequest: req,
39 | rawResponse: res,
40 | });
41 |
42 | //SWITCHCASE
43 | switch (validateWebhook.topic) {
44 | case "APP_UNINSTALLED":
45 | appUninstallHandler(
46 | validateWebhook.topic,
47 | shop,
48 | rawBody,
49 | webhookId,
50 | apiVersion
51 | );
52 | break;
53 | case "ORDERS_CREATE":
54 | case "ORDERS_UPDATED":
55 | ordersHandler(
56 | validateWebhook.topic,
57 | shop,
58 | rawBody,
59 | webhookId,
60 | apiVersion
61 | );
62 | break;
63 | default:
64 | throw new Error(`Can't find a handler for ${topic}`);
65 | }
66 | //SWITCHCASE END
67 |
68 | console.log(`--> Processed ${topic} from ${shop}`);
69 | return res.status(200).send({ message: "ok" });
70 | } catch (e) {
71 | console.error(
72 | `---> Error while processing webhooks for ${shop} at ${topic} | ${e.message}`
73 | );
74 |
75 | if (!res.headersSent) {
76 | console.error("No headers sent");
77 | }
78 | return res.status(500).send({ message: "Error" });
79 | }
80 | }
81 |
82 | export const config = {
83 | api: {
84 | bodyParser: false,
85 | },
86 | };
87 |
--------------------------------------------------------------------------------
/nextjs/pages/debug/billing.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | BlockStack,
3 | Button,
4 | Card,
5 | DataTable,
6 | InlineStack,
7 | Layout,
8 | Page,
9 | Text,
10 | } from "@shopify/polaris";
11 | import { useRouter } from "next/router";
12 | import { useEffect, useState } from "react";
13 |
14 | const BillingAPI = () => {
15 | const router = useRouter();
16 | const [responseData, setResponseData] = useState("");
17 |
18 | async function fetchContent() {
19 | setResponseData("loading...");
20 | const res = await fetch("/api/apps/debug/createNewSubscription");
21 | const data = await res.json();
22 | if (data.error) {
23 | setResponseData(data.error);
24 | } else if (data.confirmationUrl) {
25 | setResponseData("Redirecting");
26 | const { confirmationUrl } = data;
27 | open(confirmationUrl, "_top");
28 | }
29 | }
30 |
31 | return (
32 | router.push("/debug"),
37 | }}
38 | >
39 |
40 |
41 |
42 |
43 |
44 | Subscribe your merchant to a test $10.25 plan and redirect to
45 | your home page.
46 |
47 |
48 | {
49 | /* If we have an error, it'll pop up here. */
50 | responseData &&