From 3ef72bb43ea0199e496adaba855ee6ca5b6b41e9 Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Thu, 17 Jul 2025 16:21:29 -0500 Subject: [PATCH] Update template to use RR7 app template and Polaris web components --- .../delivery-customizations/.eslintignore | 1 - .../delivery-customizations/.eslintrc.cjs | 89 +++- .../delivery-customizations/.gitignore | 4 + .../delivery-customizations/.graphqlrc.ts | 4 +- .../delivery-customizations/CHANGELOG.md | 29 +- sample-apps/delivery-customizations/README.md | 213 +++------- .../delivery-customizations/app/db.server.ts | 9 +- .../app/entry.server.tsx | 14 +- .../delivery-customizations/app/root.tsx | 10 +- .../delivery-customizations/app/routes.ts | 2 +- .../app/routes/_index/route.tsx | 6 +- .../app/routes/app._index.tsx | 379 +++++++----------- .../app/routes/app.additional.tsx | 111 ++--- ...delivery-customization.$functionId.$id.tsx | 238 +++++++++++ .../app/routes/app.tsx | 15 +- .../app/routes/auth.$.tsx | 8 +- .../app/routes/auth.login/error.server.tsx | 4 +- .../app/routes/auth.login/route.tsx | 64 ++- .../app/routes/webhooks.app.scopes_update.tsx | 2 +- .../app/routes/webhooks.app.uninstalled.tsx | 2 +- .../app/shopify.server.ts | 8 +- sample-apps/delivery-customizations/env.d.ts | 2 +- .../delivery-customizations/package.json | 57 ++- .../react-router.config.ts | 4 + .../delivery-customizations/shopify.web.toml | 4 +- .../delivery-customizations/tsconfig.json | 5 +- .../delivery-customizations/vite.config.ts | 29 +- 27 files changed, 714 insertions(+), 599 deletions(-) create mode 100644 sample-apps/delivery-customizations/app/routes/app.delivery-customization.$functionId.$id.tsx create mode 100644 sample-apps/delivery-customizations/react-router.config.ts diff --git a/sample-apps/delivery-customizations/.eslintignore b/sample-apps/delivery-customizations/.eslintignore index 3796499f..d2d6eed6 100644 --- a/sample-apps/delivery-customizations/.eslintignore +++ b/sample-apps/delivery-customizations/.eslintignore @@ -1,6 +1,5 @@ node_modules build public/build -shopify-app-remix */*.yml .shopify diff --git a/sample-apps/delivery-customizations/.eslintrc.cjs b/sample-apps/delivery-customizations/.eslintrc.cjs index a42d9754..4f6f59ee 100644 --- a/sample-apps/delivery-customizations/.eslintrc.cjs +++ b/sample-apps/delivery-customizations/.eslintrc.cjs @@ -1,13 +1,84 @@ -/** @type {import('@types/eslint').Linter.BaseConfig} */ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ module.exports = { root: true, - extends: [ - "@remix-run/eslint-config", - "@remix-run/eslint-config/node", - "@remix-run/eslint-config/jest-testing-library", - "prettier", - ], - globals: { - shopify: "readonly" + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], }; diff --git a/sample-apps/delivery-customizations/.gitignore b/sample-apps/delivery-customizations/.gitignore index eab8b7bd..9ddf1b85 100644 --- a/sample-apps/delivery-customizations/.gitignore +++ b/sample-apps/delivery-customizations/.gitignore @@ -22,3 +22,7 @@ pnpm-lock.yaml # Ignore shopify files created during app dev .shopify/* .shopify.lock + +# Hide files auto-generated by react router +.react-router/ + diff --git a/sample-apps/delivery-customizations/.graphqlrc.ts b/sample-apps/delivery-customizations/.graphqlrc.ts index ad9c5f81..a28be7a2 100644 --- a/sample-apps/delivery-customizations/.graphqlrc.ts +++ b/sample-apps/delivery-customizations/.graphqlrc.ts @@ -37,4 +37,6 @@ function getConfig() { return config; } -module.exports = getConfig(); +const config = getConfig(); + +export default config; diff --git a/sample-apps/delivery-customizations/CHANGELOG.md b/sample-apps/delivery-customizations/CHANGELOG.md index b2961a35..09bc4f4b 100644 --- a/sample-apps/delivery-customizations/CHANGELOG.md +++ b/sample-apps/delivery-customizations/CHANGELOG.md @@ -1,9 +1,35 @@ +# @shopify/shopify-app-template-react-router + +## July 2025 + +Forked the [shopify-app-template repo](https://github.com/Shopify/shopify-app-template-remix) + # @shopify/shopify-app-template-remix +## 2025.03.18 + +-[#998](https://github.com/Shopify/shopify-app-template-remix/pull/998) Update to Vite 6 + +## 2025.03.01 + +- [#982](https://github.com/Shopify/shopify-app-template-remix/pull/982) Add Shopify Dev Assistant extension to the VSCode extension recommendations + +## 2025.01.31 + +- [#952](https://github.com/Shopify/shopify-app-template-remix/pull/952) Update to Shopify App API v2025-01 + +## 2025.01.23 + +- [#923](https://github.com/Shopify/shopify-app-template-remix/pull/923) Update `@shopify/shopify-app-session-storage-prisma` to v6.0.0 + +## 2025.01.8 + +- [#923](https://github.com/Shopify/shopify-app-template-remix/pull/923) Enable GraphQL autocomplete for Javascript + ## 2024.12.19 - [#904](https://github.com/Shopify/shopify-app-template-remix/pull/904) bump `@shopify/app-bridge-react` to latest -- +- ## 2024.12.18 - [875](https://github.com/Shopify/shopify-app-template-remix/pull/875) Add Scopes Update Webhook @@ -19,6 +45,7 @@ - [#891](https://github.com/Shopify/shopify-app-template-remix/pull/891) Enable remix future flags. ## 2024.11.26 + - [888](https://github.com/Shopify/shopify-app-template-remix/pull/888) Update restResources version to 2024-10 ## 2024.11.06 diff --git a/sample-apps/delivery-customizations/README.md b/sample-apps/delivery-customizations/README.md index 2789aac9..49aa9ec6 100644 --- a/sample-apps/delivery-customizations/README.md +++ b/sample-apps/delivery-customizations/README.md @@ -1,10 +1,10 @@ -# Shopify App Template - Remix +# Shopify App Template - React Router -This is a template for building a [Shopify app](https://shopify.dev/docs/apps/getting-started) using the [Remix](https://remix.run) framework. +This is a template for building a [Shopify app](https://shopify.dev/docs/apps/getting-started) using [React Router](https://reactrouter.com/). It was forked from the [Shopify Remix app template](https://github.com/Shopify/shopify-app-template-remix) and converted to React Router. Rather than cloning this repo, you can use your preferred package manager and the Shopify CLI with [these steps](https://shopify.dev/docs/apps/getting-started/create). -Visit the [`shopify.dev` documentation](https://shopify.dev/docs/api/shopify-app-remix) for more details on the Remix app package. +Visit the [`shopify.dev` documentation](https://shopify.dev/docs/api/shopify-app-react-router) for more details on the React Router app package. ## Quick start @@ -15,47 +15,21 @@ Before you begin, you'll need the following: 1. **Node.js**: [Download and install](https://nodejs.org/en/download/) it if you haven't already. 2. **Shopify Partner Account**: [Create an account](https://partners.shopify.com/signup) if you don't have one. 3. **Test Store**: Set up either a [development store](https://help.shopify.com/en/partners/dashboard/development-stores#create-a-development-store) or a [Shopify Plus sandbox store](https://help.shopify.com/en/partners/dashboard/managing-stores/plus-sandbox-store) for testing your app. - -### Setup - -If you used the CLI to create the template, you can skip this section. - -Using yarn: - -```shell -yarn install -``` - -Using npm: - +4. **Shopify CLI**: [Download and install](https://shopify.dev/docs/apps/tools/cli/getting-started) it if you haven't already. ```shell -npm install +npm install -g @shopify/cli@latest ``` -Using pnpm: +### Setup ```shell -pnpm install +shopify app init --template=https://github.com/Shopify/shopify-app-template-react-router ``` ### Local Development -Using yarn: - ```shell -yarn dev -``` - -Using npm: - -```shell -npm run dev -``` - -Using pnpm: - -```shell -pnpm run dev +shopify app dev ``` Press P to open the URL to your app. Once you click install, you can start development. @@ -90,13 +64,13 @@ export async function loader({ request }) { } ``` -This template comes preconfigured with examples of: +This template comes pre-configured with examples of: -1. Setting up your Shopify app in [/app/shopify.server.ts](https://github.com/Shopify/shopify-app-template-remix/blob/main/app/shopify.server.ts) -2. Querying data using Graphql. Please see: [/app/routes/app.\_index.tsx](https://github.com/Shopify/shopify-app-template-remix/blob/main/app/routes/app._index.tsx). -3. Responding to mandatory webhooks in [/app/routes/webhooks.tsx](https://github.com/Shopify/shopify-app-template-remix/blob/main/app/routes/webhooks.tsx) +1. Setting up your Shopify app in [/app/shopify.server.ts](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/shopify.server.ts) +2. Querying data using Graphql. Please see: [/app/routes/app.\_index.tsx](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/routes/app._index.tsx). +3. Responding to webhooks. Please see [/app/routes/webhooks.tsx](https://github.com/Shopify/shopify-app-template-react-router/blob/main/app/routes/webhooks.app.uninstalled.tsx). -Please read the [documentation for @shopify/shopify-app-remix](https://www.npmjs.com/package/@shopify/shopify-app-remix#authenticating-admin-requests) to understand what other API's are available. +Please read the [documentation for @shopify/shopify-app-react-router](https://shopify.dev/docs/api/shopify-app-react-router) to see what other API's are available. ## Deployment @@ -107,21 +81,20 @@ The database is defined as a Prisma schema in `prisma/schema.prisma`. This use of SQLite works in production if your app runs as a single instance. The database that works best for you depends on the data your app needs and how it is queried. -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: | Database | Type | Hosters | | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 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) | -| 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) | -| Redis | Key-value | [Digital Ocean](https://www.digitalocean.com/try/managed-databases-redis), [Amazon MemoryDB](https://aws.amazon.com/memorydb/) | -| MongoDB | NoSQL / Document | [Digital Ocean](https://www.digitalocean.com/try/managed-databases-mongodb), [MongoDB Atlas](https://www.mongodb.com/atlas/database) | +| MySQL | SQL | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-mysql), [Planet Scale](https://planetscale.com/), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Google Cloud SQL](https://cloud.google.com/sql/docs/mysql) | +| PostgreSQL | SQL | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-postgresql), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Google Cloud SQL](https://cloud.google.com/sql/docs/postgres) | +| Redis | Key-value | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-redis), [Amazon MemoryDB](https://aws.amazon.com/memorydb/) | +| MongoDB | NoSQL / Document | [Digital Ocean](https://www.digitalocean.com/products/managed-databases-mongodb), [MongoDB Atlas](https://www.mongodb.com/atlas/database) | To use one of these, you can use a different [datasource provider](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#datasource) in your `schema.prisma` file, or a different [SessionStorage adapter package](https://github.com/Shopify/shopify-api-js/blob/main/packages/shopify-api/docs/guides/session-storage.md). ### Build -Remix handles building the app for you, by running the command below with the package manager of your choice: +Build the app by running the command below with the package manager of your choice: Using yarn: @@ -147,64 +120,32 @@ When you're ready to set up your app in production, you can follow [our deployme When you reach the step for [setting up environment variables](https://shopify.dev/docs/apps/deployment/web#set-env-vars), you also need to set the variable `NODE_ENV=production`. -### Hosting on Vercel - -Using the Vercel Preset is recommended when hosting your Shopify Remix app on Vercel. You'll also want to ensure imports that would normally come from `@remix-run/node` are imported from `@vercel/remix` instead. Learn more about hosting Remix apps on Vercel [here](https://vercel.com/docs/frameworks/remix). - -```diff -// vite.config.ts -import { vitePlugin as remix } from "@remix-run/dev"; -import { defineConfig, type UserConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; -+ import { vercelPreset } from '@vercel/remix/vite'; - -installGlobals(); - -export default defineConfig({ - plugins: [ - remix({ - ignoredRouteFiles: ["**/.*"], -+ presets: [vercelPreset()], - }), - tsconfigPaths(), - ], -}); -``` ## Gotchas / Troubleshooting ### Database tables don't exist -If you get this error: +If you get an error like: ``` The table `main.Session` does not exist in the current database. ``` -You need to create the database for Prisma. Run the `setup` script in `package.json` using your preferred package manager. +Create the database for Prisma. Run the `setup` script in `package.json` using `npm`, `yarn` or `pnpm`. ### Navigating/redirecting breaks an embedded app -Embedded Shopify apps must maintain the user session, which can be tricky inside an iFrame. To avoid issues: - -1. Use `Link` from `@remix-run/react` or `@shopify/polaris`. Do not use ``. -2. Use the `redirect` helper returned from `authenticate.admin`. Do not use `redirect` from `@remix-run/node` -3. Use `useSubmit` or `
` from `@remix-run/react`. Do not use a lowercase ``. +Embedded apps must maintain the user session, which can be tricky inside an iFrame. To avoid issues: -This only applies if you app is embedded, which it will be by default. +1. Use `Link` from `react-router` or `@shopify/polaris`. Do not use ``. +2. Use `redirect` returned from `authenticate.admin`. Do not use `redirect` from `react-router` +3. Use `useSubmit` from `react-router`. -### Non Embedded +This only applies if your app is embedded, which it will be by default. -Shopify apps are best when they are embedded into the Shopify Admin. This template is configured that way. If you have a reason to not embed your please make 2 changes: +### App goes into a loop when I change my app's scopes -1. Change the `isEmbeddedApp` prop to false for the `AppProvider` in `/app/routes/app.jsx` -2. Remove any use of App Bridge APIs (`window.shopify`) from your code -3. Update the config for shopifyApp in `app/shopify.server.js`. Pass `isEmbeddedApp: false` - -### OAuth goes into a loop when I change my app's scopes - -If you change your app's scopes and authentication goes into a loop and fails with a message from Shopify that it tried too many times, you might have forgotten to update your scopes with Shopify. -To do that, you can run the `deploy` CLI command. +If you change your app's scopes and authentication goes into a loop before failing after trying too many times, you might have forgotten to update your scopes with Shopify. Update your scopes. Using yarn: @@ -224,62 +165,48 @@ Using pnpm: pnpm run deploy ``` -### My shop-specific webhook subscriptions aren't updated +### Webhooks: shop-specific webhook subscriptions aren't updated If you are registering webhooks in the `afterAuth` hook, using `shopify.registerWebhooks`, you may find that your subscriptions aren't being updated. -Instead of using the `afterAuth` hook, the recommended approach is to declare app-specific webhooks in the `shopify.app.toml` file. This approach is easier since Shopify will automatically update changes to webhook subscriptions every time you run `deploy` (e.g: `npm run deploy`). Please read these guides to understand more: +Instead of using the `afterAuth` hook declare app-specific webhooks in the `shopify.app.toml` file. This approach is easier since Shopify will automatically sync changes every time you run `deploy` (e.g: `npm run deploy`). Please read these guides to understand more: 1. [app-specific vs shop-specific webhooks](https://shopify.dev/docs/apps/build/webhooks/subscribe#app-specific-subscriptions) 2. [Create a subscription tutorial](https://shopify.dev/docs/apps/build/webhooks/subscribe/get-started?framework=remix&deliveryMethod=https) -If you do need shop-specific webhooks, please keep in mind that the package calls `afterAuth` in 2 scenarios: +If you do need shop-specific webhooks, keep in mind that the package calls `afterAuth` in 2 scenarios: - After installing the app - When an access token expires -During normal development, the app won't need to re-authenticate most of the time, so shop-specific subscriptions aren't updated. To force your app to update the subscriptions, you can uninstall and reinstall it in your development store. That will force the OAuth process and call the `afterAuth` hook. +During normal development, the app won't need to re-authenticate most of the time, so shop-specific subscriptions aren't updated. To force your app to update the subscriptions, uninstall and reinstall the app. Revisiting the app will call the `afterAuth` hook. + +### Webhooks: Admin created webhook failing HMAC validation -### Admin created webhook failing HMAC validation +Webhooks subscriptions created in the [Shopify admin](https://help.shopify.com/en/manual/orders/notifications/webhooks) will fail HMAC validation. This is because the webhook payload is not signed with your app's secret key. -Webhooks subscriptions created in the [Shopify admin](https://help.shopify.com/en/manual/orders/notifications/webhooks) will fail HMAC validation. This is because the webhook payload is not signed with your app's secret key. There are 2 solutions: +The recommended solution is to use [app-specific webhooks](https://shopify.dev/docs/apps/build/webhooks/subscribe#app-specific-subscriptions) defined in your toml file instead. Test your webhooks by triggering events manually in the Shopify admin(e.g. Updating the product title to trigger a `PRODUCTS_UPDATE`). -1. Use [app-specific webhooks](https://shopify.dev/docs/apps/build/webhooks/subscribe#app-specific-subscriptions) defined in your toml file instead (recommended) -2. Create [webhook subscriptions](https://shopify.dev/docs/api/shopify-app-remix/v1/guide-webhooks) using the `shopifyApp` object. +### Webhooks: Admin object undefined on webhook events triggered by the CLI -Test your webhooks with the [Shopify CLI](https://shopify.dev/docs/apps/tools/cli/commands#webhook-trigger) or by triggering events manually in the Shopify admin(e.g. Updating the product title to trigger a `PRODUCTS_UPDATE`). +When you trigger a webhook event using the Shopify CLI, the `admin` object will be `undefined`. This is because the CLI triggers an event with a valid, but non-existent, shop. The `admin` object is only available when the webhook is triggered by a shop that has installed the app. This is expected. + +Webhooks triggered by the CLI are intended for initial experimentation testing of your webhook configuration. For more information on how to test your webhooks, see the [Shopify CLI documentation](https://shopify.dev/docs/apps/tools/cli/commands#webhook-trigger). ### Incorrect GraphQL Hints -By default the [graphql.vscode-graphql](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) extension for VS Code will assume that GraphQL queries or mutations are for the [Shopify Admin API](https://shopify.dev/docs/api/admin). This is a sensible default, but it may not be true if: +By default the [graphql.vscode-graphql](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) extension for will assume that GraphQL queries or mutations are for the [Shopify Admin API](https://shopify.dev/docs/api/admin). This is a sensible default, but it may not be true if: 1. You use another Shopify API such as the storefront API. 2. You use a third party GraphQL API. -in this situation, please update the [.graphqlrc.ts](https://github.com/Shopify/shopify-app-template-remix/blob/main/.graphqlrc.ts) config. - -### First parameter has member 'readable' that is not a ReadableStream. - -See [hosting on Vercel](#hosting-on-vercel). - -### Admin object undefined on webhook events triggered by the CLI - -When you trigger a webhook event using the Shopify CLI, the `admin` object will be `undefined`. This is because the CLI triggers an event with a valid, but non-existent, shop. The `admin` object is only available when the webhook is triggered by a shop that has installed the app. - -Webhooks triggered by the CLI are intended for initial experimentation testing of your webhook configuration. For more information on how to test your webhooks, see the [Shopify CLI documentation](https://shopify.dev/docs/apps/tools/cli/commands#webhook-trigger). +If so, please update [.graphqlrc.ts](https://github.com/Shopify/shopify-app-template-react-router/blob/main/.graphqlrc.ts). ### Using Defer & await for streaming responses -To test [streaming using defer/await](https://remix.run/docs/en/main/guides/streaming) during local development you'll need to use the Shopify CLI slightly differently: - -1. First setup ngrok: https://ngrok.com/product/secure-tunnels -2. Create an ngrok tunnel on port 8080: `ngrok http 8080`. -3. Copy the forwarding address. This should be something like: `https://f355-2607-fea8-bb5c-8700-7972-d2b5-3f2b-94ab.ngrok-free.app` -4. In a separate terminal run `yarn shopify app dev --tunnel-url=TUNNEL_URL:8080` replacing `TUNNEL_URL` for the address you copied in step 3. +By default the CLI uses a cloudflare tunnel. Unfortunately cloudflare tunnels wait for the Response stream to finish, then sends one chunk. This will not affect production. -By default the CLI uses a cloudflare tunnel. Unfortunately it cloudflare tunnels wait for the Response stream to finish, then sends one chunk. - -This will not affect production, since tunnels are only for local development. +To test [streaming using await](https://reactrouter.com/api/components/Await#await) during local development we recommend [localhost based development](https://shopify.dev/docs/apps/build/cli-for-apps/networking-options#localhost-based-development). ### Using MongoDB and Prisma @@ -289,10 +216,10 @@ Alternatively you can use a MongDB database directly with the [MongoDB session s #### Mapping the id field -In MongoDB, an ID must be a single field that defines an @id attribute and a @map("\_id") attribute. +In MongoDB, an ID must be a single field that defines an `@id` attribute and a `@map("\_id")` attribute. The prisma adapter expects the ID field to be the ID of the session, and not the \_id field of the document. -To make this work you can add a new field to the schema that maps the \_id field to the id field. For more information see the [Prisma documentation](https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-an-id-field) +To make this work add a new field to the schema that maps the \_id field to the id field. For more information see the [Prisma documentation](https://www.prisma.io/docs/orm/prisma-schema/data-model/models#defining-an-id-field) ```prisma model Session { @@ -309,54 +236,34 @@ MongoDB does not support the [prisma migrate](https://www.prisma.io/docs/orm/pri ```toml [commands] predev = "npx prisma generate && npx prisma db push" -dev = "npm exec remix vite:dev" +dev = "npx prisma migrate deploy && npm exec react-router dev" ``` #### Prisma needs to perform transactions, which requires your mongodb server to be run as a replica set See the [Prisma documentation](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/mongodb/connect-your-database-node-mongodb) for connecting to a MongoDB database. -### I want to use Polaris v13.0.0 or higher - -Currently, this template is set up to work on node v18.20 or higher. However, `@shopify/polaris` is limited to v12 because v13 can only run on node v20+. - -You don't have to make any changes to the code in order to be able to upgrade Polaris to v13, but you'll need to do the following: - -- Upgrade your node version to v20.10 or higher. -- Update your `Dockerfile` to pull `FROM node:20-alpine` instead of `node:18-alpine` - -## Benefits +### "nbf" claim timestamp check failed -Shopify apps are built on a variety of Shopify tools to create a great merchant experience. +This is because a JWT token is expired. If you are consistently getting this error, it could be that the clock on your machine is not in sync with the server. To fix this ensure you have enabled "Set time and date automatically" in the "Date and Time" settings on your computer. - - -The Remix app template comes with the following out-of-the-box functionality: - -- [OAuth](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-remix#authenticating-admin-requests): Installing the app and granting permissions -- [GraphQL Admin API](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-remix#using-the-shopify-admin-graphql-api): Querying or mutating Shopify admin data -- [Webhooks](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-remix#authenticating-webhook-requests): Callbacks sent by Shopify when certain events occur -- [AppBridge](https://shopify.dev/docs/api/app-bridge): This template uses the next generation of the Shopify App Bridge library which works in unison with previous versions. -- [Polaris](https://polaris.shopify.com/): Design system that enables apps to create Shopify-like experiences - -## Tech Stack +## Resources -This template uses [Remix](https://remix.run). The following Shopify tools are also included to ease app development: +React Router: -- [Shopify App Remix](https://shopify.dev/docs/api/shopify-app-remix) provides authentication and methods for interacting with Shopify APIs. -- [Shopify App Bridge](https://shopify.dev/docs/apps/tools/app-bridge) allows your app to seamlessly integrate your app within Shopify's Admin. -- [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. -- [Webhooks](https://github.com/Shopify/shopify-app-js/tree/main/packages/shopify-app-remix#authenticating-webhook-requests): Callbacks sent by Shopify when certain events occur -- [Polaris](https://polaris.shopify.com/): Design system that enables apps to create Shopify-like experiences +- [React Router docs](https://reactrouter.com/home) -## Resources +Shopify: -- [Remix Docs](https://remix.run/docs/en/v1) -- [Shopify App Remix](https://shopify.dev/docs/api/shopify-app-remix) -- [Introduction to Shopify apps](https://shopify.dev/docs/apps/getting-started) -- [App authentication](https://shopify.dev/docs/apps/auth) +- [Intro to Shopify apps](https://shopify.dev/docs/apps/getting-started) +- [Shopify App React Router docs](https://shopify.dev/docs/api/shopify-app-react-router) - [Shopify CLI](https://shopify.dev/docs/apps/tools/cli) +- [Shopify App Bridge](https://shopify.dev/docs/api/app-bridge-library). +- [Polaris Web Components](https://shopify.dev/docs/api/app-home/polaris-web-components). - [App extensions](https://shopify.dev/docs/apps/app-extensions/list) - [Shopify Functions](https://shopify.dev/docs/api/functions) -- [Getting started with internationalizing your app](https://shopify.dev/docs/apps/best-practices/internationalization/getting-started) + +Internationalization: + +- [Internationalizing your app](https://shopify.dev/docs/apps/best-practices/internationalization/getting-started) diff --git a/sample-apps/delivery-customizations/app/db.server.ts b/sample-apps/delivery-customizations/app/db.server.ts index bafa6cc2..586c54b0 100644 --- a/sample-apps/delivery-customizations/app/db.server.ts +++ b/sample-apps/delivery-customizations/app/db.server.ts @@ -1,15 +1,16 @@ import { PrismaClient } from "@prisma/client"; declare global { - var prisma: PrismaClient; + // eslint-disable-next-line no-var + var prismaGlobal: PrismaClient; } if (process.env.NODE_ENV !== "production") { - if (!global.prisma) { - global.prisma = new PrismaClient(); + if (!global.prismaGlobal) { + global.prismaGlobal = new PrismaClient(); } } -const prisma: PrismaClient = global.prisma || new PrismaClient(); +const prisma = global.prismaGlobal ?? new PrismaClient(); export default prisma; diff --git a/sample-apps/delivery-customizations/app/entry.server.tsx b/sample-apps/delivery-customizations/app/entry.server.tsx index 86274311..b9cf2263 100644 --- a/sample-apps/delivery-customizations/app/entry.server.tsx +++ b/sample-apps/delivery-customizations/app/entry.server.tsx @@ -1,10 +1,8 @@ import { PassThrough } from "stream"; import { renderToPipeableStream } from "react-dom/server"; -import { RemixServer } from "@remix-run/react"; -import { - createReadableStreamFromReadable, - type EntryContext, -} from "@remix-run/node"; +import { ServerRouter } from "react-router"; +import { createReadableStreamFromReadable } from "@react-router/node"; +import { type EntryContext } from "react-router"; import { isbot } from "isbot"; import { addDocumentResponseHeaders } from "./shopify.server"; @@ -14,7 +12,7 @@ export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, - remixContext: EntryContext + reactRouterContext: EntryContext ) { addDocumentResponseHeaders(request, responseHeaders); const userAgent = request.headers.get("user-agent"); @@ -24,8 +22,8 @@ export default async function handleRequest( return new Promise((resolve, reject) => { const { pipe, abort } = renderToPipeableStream( - , { diff --git a/sample-apps/delivery-customizations/app/root.tsx b/sample-apps/delivery-customizations/app/root.tsx index 805f121f..743dd05e 100644 --- a/sample-apps/delivery-customizations/app/root.tsx +++ b/sample-apps/delivery-customizations/app/root.tsx @@ -1,14 +1,8 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; export default function App() { return ( - + diff --git a/sample-apps/delivery-customizations/app/routes.ts b/sample-apps/delivery-customizations/app/routes.ts index 83892841..dc7d046f 100644 --- a/sample-apps/delivery-customizations/app/routes.ts +++ b/sample-apps/delivery-customizations/app/routes.ts @@ -1,3 +1,3 @@ -import { flatRoutes } from "@remix-run/fs-routes"; +import { flatRoutes } from "@react-router/fs-routes"; export default flatRoutes(); diff --git a/sample-apps/delivery-customizations/app/routes/_index/route.tsx b/sample-apps/delivery-customizations/app/routes/_index/route.tsx index 2de9dd47..ed27a27a 100644 --- a/sample-apps/delivery-customizations/app/routes/_index/route.tsx +++ b/sample-apps/delivery-customizations/app/routes/_index/route.tsx @@ -1,6 +1,6 @@ -import type { LoaderFunctionArgs } from "@remix-run/node"; -import { redirect } from "@remix-run/node"; -import { Form, useLoaderData } from "@remix-run/react"; +import type { LoaderFunctionArgs } from "react-router"; +import { redirect } from "react-router"; +import { Form, useLoaderData } from "react-router"; import { login } from "../../shopify.server"; diff --git a/sample-apps/delivery-customizations/app/routes/app._index.tsx b/sample-apps/delivery-customizations/app/routes/app._index.tsx index 18b215b0..054d6f65 100644 --- a/sample-apps/delivery-customizations/app/routes/app._index.tsx +++ b/sample-apps/delivery-customizations/app/routes/app._index.tsx @@ -1,20 +1,13 @@ import { useEffect } from "react"; -import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; -import { useFetcher } from "@remix-run/react"; -import { - Page, - Layout, - Text, - Card, - Button, - BlockStack, - Box, - List, - Link, - InlineStack, -} from "@shopify/polaris"; +import type { + ActionFunctionArgs, + HeadersFunction, + LoaderFunctionArgs, +} from "react-router"; +import { useFetcher } from "react-router"; import { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; import { authenticate } from "../shopify.server"; +import { boundary } from "@shopify/shopify-app-react-router/server"; export const loader = async ({ request }: LoaderFunctionArgs) => { await authenticate.admin(request); @@ -64,7 +57,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const variantResponse = await admin.graphql( `#graphql - mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { + mutation shopifyReactRouterTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id @@ -111,224 +104,150 @@ export default function Index() { const generateProduct = () => fetcher.submit({}, { method: "POST" }); return ( - - + + - - - - - - - - Congrats on creating a new Shopify app 🎉 - - - This embedded app template uses{" "} - - App Bridge - {" "} - interface examples like an{" "} - - additional page in the app nav - - , as well as an{" "} - - Admin GraphQL - {" "} - mutation demo, to provide a starting point for app - development. - - - - - Get started with products - - - Generate a product with GraphQL and get the JSON output for - that product. Learn more about the{" "} - - productCreate - {" "} - mutation in our API references. - - - - - {fetcher.data?.product && ( - - )} - - {fetcher.data?.product && ( - <> - - {" "} - productCreate mutation - - -
-                        
-                          {JSON.stringify(fetcher.data.product, null, 2)}
-                        
-                      
-
- - {" "} - productVariantsBulkUpdate mutation - - -
-                        
-                          {JSON.stringify(fetcher.data.variant, null, 2)}
-                        
-                      
-
- - )} -
-
-
- - - - - - App template specs - - - - - Framework - - - Remix - - - - - Database - - - Prisma - - - - - Interface - - - - Polaris - - {", "} - - App Bridge - - - - - - API - - - GraphQL API - - - - - - - - - Next steps - - - - Build an{" "} - - {" "} - example app - {" "} - to get started - - - Explore Shopify’s API with{" "} - - GraphiQL - - - - - - - -
-
-
+ + + This embedded app template uses{" "} + + App Bridge + {" "} + interface examples like an{" "} + additional page in the app nav + , as well as an{" "} + + Admin GraphQL + {" "} + mutation demo, to provide a starting point for app development. + + + + + Generate a product with GraphQL and get the JSON output for that + product. Learn more about the{" "} + + productCreate + {" "} + mutation in our API references. + + + + Generate a product + + {fetcher.data?.product && ( + + View product + + )} + + {fetcher.data?.product && ( + + + +
+                  {JSON.stringify(fetcher.data.product, null, 2)}
+                
+
+ + productVariantsBulkUpdate mutation + +
+                  {JSON.stringify(fetcher.data.variant, null, 2)}
+                
+
+
+
+ )} +
+ + + + Framework: + + React Router + + + + Database: + + Prisma + + + + Interface: + + Polaris + + + + API: + + GraphQL + + + + + + + + Build an{" "} + + example app + + + + Explore Shopify's API with{" "} + + GraphiQL + + + + + ); } + +export const headers: HeadersFunction = (headersArgs) => { + return boundary.headers(headersArgs); +}; diff --git a/sample-apps/delivery-customizations/app/routes/app.additional.tsx b/sample-apps/delivery-customizations/app/routes/app.additional.tsx index eb9b0cfd..66c68cd3 100644 --- a/sample-apps/delivery-customizations/app/routes/app.additional.tsx +++ b/sample-apps/delivery-customizations/app/routes/app.additional.tsx @@ -1,83 +1,40 @@ -import { - Box, - Card, - Layout, - Link, - List, - Page, - Text, - BlockStack, -} from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; export default function AdditionalPage() { return ( - - - - - - - - The app template comes with an additional page which - demonstrates how to create multiple pages within app navigation - using{" "} - - App Bridge - - . - - - To create your own page and have it show up in the app - navigation, add a page inside app/routes, and a - link to it in the <NavMenu> component found - in app/routes/app.jsx. - - - - - - - - - Resources - - - - - App nav best practices - - - - - - - - - ); -} - -function Code({ children }: { children: React.ReactNode }) { - return ( - - {children} - + + + + + The app template comes with an additional page which demonstrates how + to create multiple pages within app navigation using{" "} + + App Bridge + + . + + + To create your own page and have it show up in the app navigation, add + a page inside app/routes, and a link to it in the{" "} + <NavMenu> component found in{" "} + app/routes/app.jsx. + + + + + + + App nav best practices + + + + + ); } diff --git a/sample-apps/delivery-customizations/app/routes/app.delivery-customization.$functionId.$id.tsx b/sample-apps/delivery-customizations/app/routes/app.delivery-customization.$functionId.$id.tsx new file mode 100644 index 00000000..0ab0e56d --- /dev/null +++ b/sample-apps/delivery-customizations/app/routes/app.delivery-customization.$functionId.$id.tsx @@ -0,0 +1,238 @@ +import { useState, useEffect } from "react"; +import type { FormEvent } from "react"; +import { + useActionData, + useNavigation, + useSubmit, + useLoaderData, +} from "react-router"; +import type { LoaderFunctionArgs, ActionFunctionArgs } from "react-router"; +import { authenticate } from "../shopify.server"; + +interface LoaderData { + headers: { "Content-Type": string }; + body: string; +} + +interface ActionData { + errors: Array<{ message: string }>; +} + +interface DeliveryCustomizationData { + stateProvinceCode: string; + message: string; +} + +export const loader = async ({ params, request }: LoaderFunctionArgs): Promise => { + const { id } = params; + const { admin } = await authenticate.admin(request); + + if (id !== "new") { + const gid = `gid://shopify/DeliveryCustomization/${id}`; + + const response = await admin.graphql( + `#graphql + query getDeliveryCustomization($id: ID!) { + deliveryCustomization(id: $id) { + id + title + enabled + metafield(namespace: "$app:delivery-customization", key: "function-configuration") { + id + value + } + } + }`, + { + variables: { + id: gid, + }, + } + ); + + const responseJson = await response.json(); + const deliveryCustomization = responseJson.data.deliveryCustomization; + const metafieldValue = JSON.parse(deliveryCustomization.metafield.value); + + return { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + stateProvinceCode: metafieldValue.stateProvinceCode, + message: metafieldValue.message, + }), + }; + } + + return { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + stateProvinceCode: "", + message: "", + }), + }; +}; + +export const action = async ({ params, request }: ActionFunctionArgs): Promise => { + const { functionId, id } = params; + const { admin } = await authenticate.admin(request); + const formData = await request.formData(); + + const stateProvinceCode = formData.get("stateProvinceCode") as string; + const message = formData.get("message") as string; + + const deliveryCustomizationInput = { + functionId, + title: `Change ${stateProvinceCode} delivery message`, + enabled: true, + metafields: [ + { + namespace: "$app:delivery-customization", + key: "function-configuration", + type: "json", + value: JSON.stringify({ + stateProvinceCode, + message, + }), + }, + ], + }; + + if (id !== "new") { + const response = await admin.graphql( + `#graphql + mutation updateDeliveryCustomization($id: ID!, $input: DeliveryCustomizationInput!) { + deliveryCustomizationUpdate(id: $id, deliveryCustomization: $input) { + deliveryCustomization { + id + } + userErrors { + message + } + } + }`, + { + variables: { + id: `gid://shopify/DeliveryCustomization/${id}`, + input: deliveryCustomizationInput, + }, + } + ); + + const responseJson = await response.json(); + const errors = responseJson.data.deliveryCustomizationUpdate?.userErrors; + + return { errors }; + } else { + const response = await admin.graphql( + `#graphql + mutation createDeliveryCustomization($input: DeliveryCustomizationInput!) { + deliveryCustomizationCreate(deliveryCustomization: $input) { + deliveryCustomization { + id + } + userErrors { + message + } + } + }`, + { + variables: { + input: deliveryCustomizationInput, + }, + } + ); + + const responseJson = await response.json(); + const errors = responseJson.data.deliveryCustomizationCreate?.userErrors; + + return { errors }; + } +}; + +export default function DeliveryCustomization() { + const submit = useSubmit(); + const actionData = useActionData(); + const navigation = useNavigation(); + const loaderData = useLoaderData(); + + const parsedLoaderData: DeliveryCustomizationData = loaderData?.body + ? JSON.parse(loaderData.body) + : { stateProvinceCode: "", message: "" }; + + const [stateProvinceCode, setStateProvinceCode] = useState(parsedLoaderData.stateProvinceCode); + const [message, setMessage] = useState(parsedLoaderData.message); + + useEffect(() => { + if (loaderData?.body) { + const parsedData: DeliveryCustomizationData = JSON.parse(loaderData.body); + setStateProvinceCode(parsedData.stateProvinceCode || ""); + setMessage(parsedData.message || ""); + } + }, [loaderData]); + + const isLoading = navigation.state === "submitting"; + + useEffect(() => { + if (actionData?.errors.length === 0) { + open('shopify:admin/settings/shipping/customizations', '_top') + } + }, [actionData?.errors]); + + const errorBanner = actionData?.errors.length ? ( + +
    + {actionData?.errors.map((error, index) => ( +
  • {error.message}
  • + ))} +
+
+ ) : null; + +const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + submit({ stateProvinceCode, message }, { method: "post" }); +}; + +const handleReset = () => { + setStateProvinceCode(parsedLoaderData.stateProvinceCode); + setMessage(parsedLoaderData.message); +}; + +return ( + + + + + + + {errorBanner} + + + + setStateProvinceCode((e.target as HTMLInputElement).value)} + > + + setMessage((e.target as HTMLInputElement).value)} + > + + + + +); +} \ No newline at end of file diff --git a/sample-apps/delivery-customizations/app/routes/app.tsx b/sample-apps/delivery-customizations/app/routes/app.tsx index bdcf1162..8a305fac 100644 --- a/sample-apps/delivery-customizations/app/routes/app.tsx +++ b/sample-apps/delivery-customizations/app/routes/app.tsx @@ -1,14 +1,11 @@ -import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node"; -import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react"; -import { boundary } from "@shopify/shopify-app-remix/server"; -import { AppProvider } from "@shopify/shopify-app-remix/react"; +import type { HeadersFunction, LoaderFunctionArgs } from "react-router"; +import { Link, Outlet, useLoaderData, useRouteError } from "react-router"; +import { boundary } from "@shopify/shopify-app-react-router/server"; import { NavMenu } from "@shopify/app-bridge-react"; -import polarisStyles from "@shopify/polaris/build/esm/styles.css?url"; +import { AppProvider } from "@shopify/shopify-app-react-router/react"; import { authenticate } from "../shopify.server"; -export const links = () => [{ rel: "stylesheet", href: polarisStyles }]; - export const loader = async ({ request }: LoaderFunctionArgs) => { await authenticate.admin(request); @@ -19,7 +16,7 @@ export default function App() { const { apiKey } = useLoaderData(); return ( - + Home @@ -31,7 +28,7 @@ export default function App() { ); } -// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response. +// Shopify needs React Router to catch some thrown responses, so that their headers are included in the response. export function ErrorBoundary() { return boundary.error(useRouteError()); } diff --git a/sample-apps/delivery-customizations/app/routes/auth.$.tsx b/sample-apps/delivery-customizations/app/routes/auth.$.tsx index 8919320d..734fc044 100644 --- a/sample-apps/delivery-customizations/app/routes/auth.$.tsx +++ b/sample-apps/delivery-customizations/app/routes/auth.$.tsx @@ -1,8 +1,14 @@ -import type { LoaderFunctionArgs } from "@remix-run/node"; + +import type { HeadersFunction, LoaderFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; +import { boundary } from "@shopify/shopify-app-react-router/server"; export const loader = async ({ request }: LoaderFunctionArgs) => { await authenticate.admin(request); return null; }; + +export const headers: HeadersFunction = (headersArgs) => { + return boundary.headers(headersArgs); +}; diff --git a/sample-apps/delivery-customizations/app/routes/auth.login/error.server.tsx b/sample-apps/delivery-customizations/app/routes/auth.login/error.server.tsx index 2c794974..9c016a43 100644 --- a/sample-apps/delivery-customizations/app/routes/auth.login/error.server.tsx +++ b/sample-apps/delivery-customizations/app/routes/auth.login/error.server.tsx @@ -1,5 +1,5 @@ -import type { LoginError } from "@shopify/shopify-app-remix/server"; -import { LoginErrorType } from "@shopify/shopify-app-remix/server"; +import type { LoginError } from "@shopify/shopify-app-react-router/server"; +import { LoginErrorType } from "@shopify/shopify-app-react-router/server"; interface LoginErrorMessage { shop?: string; diff --git a/sample-apps/delivery-customizations/app/routes/auth.login/route.tsx b/sample-apps/delivery-customizations/app/routes/auth.login/route.tsx index 0e9aece7..2502527b 100644 --- a/sample-apps/delivery-customizations/app/routes/auth.login/route.tsx +++ b/sample-apps/delivery-customizations/app/routes/auth.login/route.tsx @@ -1,28 +1,15 @@ import { useState } from "react"; -import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; -import { Form, useActionData, useLoaderData } from "@remix-run/react"; -import { - AppProvider as PolarisAppProvider, - Button, - Card, - FormLayout, - Page, - Text, - TextField, -} from "@shopify/polaris"; -import polarisTranslations from "@shopify/polaris/locales/en.json"; -import polarisStyles from "@shopify/polaris/build/esm/styles.css?url"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; +import { Form, useActionData, useLoaderData } from "react-router"; +import { AppProvider } from "@shopify/shopify-app-react-router/react"; import { login } from "../../shopify.server"; - import { loginErrorMessage } from "./error.server"; -export const links = () => [{ rel: "stylesheet", href: polarisStyles }]; - export const loader = async ({ request }: LoaderFunctionArgs) => { const errors = loginErrorMessage(await login(request)); - return { errors, polarisTranslations }; + return { errors }; }; export const action = async ({ request }: ActionFunctionArgs) => { @@ -40,29 +27,24 @@ export default function Auth() { const { errors } = actionData || loaderData; return ( - - - -
- - - Log in - - - - -
-
-
-
+ + +
+ + + Log in + +
+
+
); } diff --git a/sample-apps/delivery-customizations/app/routes/webhooks.app.scopes_update.tsx b/sample-apps/delivery-customizations/app/routes/webhooks.app.scopes_update.tsx index c36bb64c..b1610b8e 100644 --- a/sample-apps/delivery-customizations/app/routes/webhooks.app.scopes_update.tsx +++ b/sample-apps/delivery-customizations/app/routes/webhooks.app.scopes_update.tsx @@ -1,4 +1,4 @@ -import type { ActionFunctionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; import db from "../db.server"; diff --git a/sample-apps/delivery-customizations/app/routes/webhooks.app.uninstalled.tsx b/sample-apps/delivery-customizations/app/routes/webhooks.app.uninstalled.tsx index 54d3161c..aaf7cc81 100644 --- a/sample-apps/delivery-customizations/app/routes/webhooks.app.uninstalled.tsx +++ b/sample-apps/delivery-customizations/app/routes/webhooks.app.uninstalled.tsx @@ -1,4 +1,4 @@ -import type { ActionFunctionArgs } from "@remix-run/node"; +import type { ActionFunctionArgs } from "react-router"; import { authenticate } from "../shopify.server"; import db from "../db.server"; diff --git a/sample-apps/delivery-customizations/app/shopify.server.ts b/sample-apps/delivery-customizations/app/shopify.server.ts index ec980711..1c25fed4 100644 --- a/sample-apps/delivery-customizations/app/shopify.server.ts +++ b/sample-apps/delivery-customizations/app/shopify.server.ts @@ -1,16 +1,16 @@ -import "@shopify/shopify-app-remix/adapters/node"; +import "@shopify/shopify-app-react-router/adapters/node"; import { ApiVersion, AppDistribution, shopifyApp, -} from "@shopify/shopify-app-remix/server"; +} from "@shopify/shopify-app-react-router/server"; import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma"; import prisma from "./db.server"; const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY, apiSecretKey: process.env.SHOPIFY_API_SECRET || "", - apiVersion: ApiVersion.October24, + apiVersion: ApiVersion.January25, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL || "", authPathPrefix: "/auth", @@ -26,7 +26,7 @@ const shopify = shopifyApp({ }); export default shopify; -export const apiVersion = ApiVersion.October24; +export const apiVersion = ApiVersion.January25; export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders; export const authenticate = shopify.authenticate; export const unauthenticated = shopify.unauthenticated; diff --git a/sample-apps/delivery-customizations/env.d.ts b/sample-apps/delivery-customizations/env.d.ts index 8d2f9518..fc1bbb27 100644 --- a/sample-apps/delivery-customizations/env.d.ts +++ b/sample-apps/delivery-customizations/env.d.ts @@ -1,2 +1,2 @@ /// -/// +/// diff --git a/sample-apps/delivery-customizations/package.json b/sample-apps/delivery-customizations/package.json index 4838f7ac..b0120640 100644 --- a/sample-apps/delivery-customizations/package.json +++ b/sample-apps/delivery-customizations/package.json @@ -2,17 +2,17 @@ "name": "app", "private": true, "scripts": { - "build": "remix vite:build", + "build": "react-router build", "dev": "shopify app dev", "config:link": "shopify app config link", "generate": "shopify app generate", "deploy": "shopify app deploy", "config:use": "shopify app config use", "env": "shopify app env", - "start": "remix-serve ./build/server/index.js", + "start": "react-router-serve ./build/server/index.js", "docker-start": "npm run setup && npm run start", "setup": "prisma generate && prisma migrate deploy", - "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "shopify": "shopify", "prisma": "prisma", "graphql-codegen": "graphql-codegen", @@ -20,39 +20,43 @@ }, "type": "module", "engines": { - "node": "^18.20 || ^20.10 || >=21.0.0" + "node": ">=20.10" }, "dependencies": { - "@prisma/client": "^5.11.0", - "@remix-run/dev": "^2.7.1", - "@remix-run/fs-routes": "^2.15.0", - "@remix-run/node": "^2.7.1", - "@remix-run/react": "^2.7.1", - "@remix-run/serve": "^2.7.1", + "@prisma/client": "^6.2.1", + "@react-router/dev": "^7.0.0", + "@react-router/fs-routes": "^7.0.0", + "@react-router/node": "^7.0.0", + "@react-router/serve": "^7.0.0", "@shopify/app-bridge-react": "^4.1.6", + "@shopify/app-bridge-ui-types": "^0.1.1", "@shopify/cli": "^3.63.1", - "@shopify/polaris": "^12.0.0", - "@shopify/shopify-app-remix": "^3.4.0", - "@shopify/shopify-app-session-storage-prisma": "^5.1.5", + "@shopify/shopify-app-react-router": "^0.1.0", + "@shopify/shopify-app-session-storage-prisma": "^6.0.0", "isbot": "^5.1.0", - "prisma": "^5.11.0", + "prisma": "^6.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router": "^7.0.0", "vite-tsconfig-paths": "^5.0.1" }, "devDependencies": { - "@remix-run/eslint-config": "^2.7.1", - "@remix-run/route-config": "^2.15.0", "@shopify/api-codegen-preset": "^1.1.1", - "@types/eslint": "^8.40.0", + "@types/eslint": "^9.6.1", "@types/node": "^22.2.0", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.1.0", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^3.2.4", "typescript": "^5.2.2", - "vite": "^5.1.3" + "vite": "^6.2.2" }, "workspaces": { "packages": [ @@ -62,6 +66,15 @@ "trustedDependencies": [ "@shopify/plugin-cloudflare" ], - "resolutions": {}, - "overrides": {} + "resolutions": { + "@graphql-tools/url-loader": "8.0.16", + "@graphql-codegen/client-preset": "4.7.0", + "@graphql-codegen/typescript-operations": "4.5.0" + }, + "overrides": { + "@graphql-tools/url-loader": "8.0.16", + "@graphql-codegen/client-preset": "4.7.0", + "@graphql-codegen/typescript-operations": "4.5.0" + }, + "packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1" } diff --git a/sample-apps/delivery-customizations/react-router.config.ts b/sample-apps/delivery-customizations/react-router.config.ts new file mode 100644 index 00000000..6bdd6fc5 --- /dev/null +++ b/sample-apps/delivery-customizations/react-router.config.ts @@ -0,0 +1,4 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, +} satisfies Config; diff --git a/sample-apps/delivery-customizations/shopify.web.toml b/sample-apps/delivery-customizations/shopify.web.toml index b35c57be..b0a0f29e 100644 --- a/sample-apps/delivery-customizations/shopify.web.toml +++ b/sample-apps/delivery-customizations/shopify.web.toml @@ -1,7 +1,7 @@ -name = "remix" +name = "React Router" roles = ["frontend", "backend"] webhooks_path = "/webhooks/app/uninstalled" [commands] predev = "npx prisma generate" -dev = "npx prisma migrate deploy && npm exec remix vite:dev" +dev = "npx prisma migrate deploy && npm exec react-router dev" diff --git a/sample-apps/delivery-customizations/tsconfig.json b/sample-apps/delivery-customizations/tsconfig.json index 7c89723f..7bf495ae 100644 --- a/sample-apps/delivery-customizations/tsconfig.json +++ b/sample-apps/delivery-customizations/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "strict": true, @@ -16,6 +16,7 @@ "moduleResolution": "Bundler", "target": "ES2022", "baseUrl": ".", - "types": ["node"] + "types": ["@react-router/node", "vite/client"], + "rootDirs": [".", "./.react-router/types"] } } diff --git a/sample-apps/delivery-customizations/vite.config.ts b/sample-apps/delivery-customizations/vite.config.ts index 82142f42..0b32e25b 100644 --- a/sample-apps/delivery-customizations/vite.config.ts +++ b/sample-apps/delivery-customizations/vite.config.ts @@ -1,13 +1,11 @@ -import { vitePlugin as remix } from "@remix-run/dev"; -import { installGlobals } from "@remix-run/node"; +import { reactRouter } from "@react-router/dev/vite"; import { defineConfig, type UserConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -installGlobals({ nativeFetch: true }); - // Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176 -// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually -// stop passing in HOST, so we can remove this workaround after the next major release. +// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the Vite server. +// The CLI will eventually stop passing in HOST, +// so we can remove this workaround after the next major release. if ( process.env.HOST && (!process.env.SHOPIFY_APP_URL || @@ -39,6 +37,10 @@ if (host === "localhost") { export default defineConfig({ server: { + allowedHosts: [host], + cors: { + preflightContinue: true, + }, port: Number(process.env.PORT || 3000), hmr: hmrConfig, fs: { @@ -47,20 +49,13 @@ export default defineConfig({ }, }, plugins: [ - remix({ - ignoredRouteFiles: ["**/.*"], - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_lazyRouteDiscovery: true, - v3_singleFetch: false, - v3_routeConfig: true, - }, - }), + reactRouter(), tsconfigPaths(), ], build: { assetsInlineLimit: 0, }, + optimizeDeps: { + include: ["@shopify/app-bridge-react", "@shopify/polaris"], + }, }) satisfies UserConfig;