From 53d485b987e939d4e580258a6f7d79e1b95591d4 Mon Sep 17 00:00:00 2001
From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com>
Date: Fri, 26 Apr 2024 13:30:09 -0400
Subject: [PATCH 1/2] Merge pull request #690 from
Shopify/update_shopify_dependencies
Update Shopify dependencies and add a CI step to ensure installs work
---
.github/workflows/ci.yml | 18 ++
.graphqlrc.ts | 40 +++
app/db.server.ts | 15 ++
app/entry.server.tsx | 58 +++++
app/globals.d.ts | 1 +
app/root.tsx | 30 +++
app/routes/_index/route.tsx | 58 +++++
app/routes/app._index.tsx | 333 +++++++++++++++++++++++++
app/routes/app.additional.tsx | 82 ++++++
app/routes/app.tsx | 41 +++
app/routes/auth.$.tsx | 8 +
app/routes/auth.login/error.server.tsx | 16 ++
app/routes/auth.login/route.tsx | 69 +++++
app/routes/webhooks.tsx | 28 +++
app/shopify.server.ts | 51 ++++
package.json | 6 +-
vite.config.ts | 55 ++++
17 files changed, 906 insertions(+), 3 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .graphqlrc.ts
create mode 100644 app/db.server.ts
create mode 100644 app/entry.server.tsx
create mode 100644 app/globals.d.ts
create mode 100644 app/root.tsx
create mode 100644 app/routes/_index/route.tsx
create mode 100644 app/routes/app._index.tsx
create mode 100644 app/routes/app.additional.tsx
create mode 100644 app/routes/app.tsx
create mode 100644 app/routes/auth.$.tsx
create mode 100644 app/routes/auth.login/error.server.tsx
create mode 100644 app/routes/auth.login/route.tsx
create mode 100644 app/routes/webhooks.tsx
create mode 100644 app/shopify.server.ts
create mode 100644 vite.config.ts
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..8181f207
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,18 @@
+on: [push, pull_request]
+
+name: CI
+
+jobs:
+ CI:
+ name: CI_Node_${{ matrix.version }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ version: [18, 20]
+ steps:
+ - uses: actions/checkout@master
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.version }}
+ - name: Install
+ run: yarn install
diff --git a/.graphqlrc.ts b/.graphqlrc.ts
new file mode 100644
index 00000000..23c8d1bc
--- /dev/null
+++ b/.graphqlrc.ts
@@ -0,0 +1,40 @@
+import fs from "fs";
+import { LATEST_API_VERSION } from "@shopify/shopify-api";
+import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
+import type { IGraphQLConfig } from "graphql-config";
+
+function getConfig() {
+ const config: IGraphQLConfig = {
+ projects: {
+ default: shopifyApiProject({
+ apiType: ApiType.Admin,
+ apiVersion: LATEST_API_VERSION,
+ documents: ["./app/**/*.{js,ts,jsx,tsx}"],
+ outputDir: "./app/types",
+ }),
+ },
+ };
+
+ let extensions: string[] = [];
+ try {
+ extensions = fs.readdirSync("./extensions");
+ } catch {
+ // ignore if no extensions
+ }
+
+ for (const entry of extensions) {
+ const extensionPath = `./extensions/${entry}`;
+ const schema = `${extensionPath}/schema.graphql`;
+ if (!fs.existsSync(schema)) {
+ continue;
+ }
+ config.projects[entry] = {
+ schema,
+ documents: [`${extensionPath}/**/*.graphql`],
+ };
+ }
+
+ return config;
+}
+
+module.exports = getConfig();
diff --git a/app/db.server.ts b/app/db.server.ts
new file mode 100644
index 00000000..e92f23e6
--- /dev/null
+++ b/app/db.server.ts
@@ -0,0 +1,15 @@
+import { PrismaClient } from "@prisma/client";
+
+declare global {
+ var prisma: PrismaClient;
+}
+
+const prisma: PrismaClient = global.prisma || new PrismaClient();
+
+if (process.env.NODE_ENV !== "production") {
+ if (!global.prisma) {
+ global.prisma = new PrismaClient();
+ }
+}
+
+export default prisma;
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
new file mode 100644
index 00000000..07860846
--- /dev/null
+++ b/app/entry.server.tsx
@@ -0,0 +1,58 @@
+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 { isbot } from "isbot";
+import { addDocumentResponseHeaders } from "./shopify.server";
+
+const ABORT_DELAY = 5000;
+
+export default async function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ addDocumentResponseHeaders(request, responseHeaders);
+ const userAgent = request.headers.get("user-agent");
+ const callbackName = isbot(userAgent ?? '')
+ ? "onAllReady"
+ : "onShellReady";
+
+ return new Promise((resolve, reject) => {
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+ pipe(body);
+ },
+ onShellError(error) {
+ reject(error);
+ },
+ onError(error) {
+ responseStatusCode = 500;
+ console.error(error);
+ },
+ }
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/app/globals.d.ts b/app/globals.d.ts
new file mode 100644
index 00000000..cbe652db
--- /dev/null
+++ b/app/globals.d.ts
@@ -0,0 +1 @@
+declare module "*.css";
diff --git a/app/root.tsx b/app/root.tsx
new file mode 100644
index 00000000..805f121f
--- /dev/null
+++ b/app/root.tsx
@@ -0,0 +1,30 @@
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx
new file mode 100644
index 00000000..cf668d31
--- /dev/null
+++ b/app/routes/_index/route.tsx
@@ -0,0 +1,58 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { json, redirect } from "@remix-run/node";
+import { Form, useLoaderData } from "@remix-run/react";
+
+import { login } from "../../shopify.server";
+
+import styles from "./styles.module.css";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const url = new URL(request.url);
+
+ if (url.searchParams.get("shop")) {
+ throw redirect(`/app?${url.searchParams.toString()}`);
+ }
+
+ return json({ showForm: Boolean(login) });
+};
+
+export default function App() {
+ const { showForm } = useLoaderData();
+
+ return (
+
+
+
A short heading about [your app]
+
+ A tagline about [your app] that describes your value proposition.
+
+ {showForm && (
+
+ )}
+
+ -
+ Product feature. Some detail about your feature and
+ its benefit to your customer.
+
+ -
+ Product feature. Some detail about your feature and
+ its benefit to your customer.
+
+ -
+ Product feature. Some detail about your feature and
+ its benefit to your customer.
+
+
+
+
+ );
+}
diff --git a/app/routes/app._index.tsx b/app/routes/app._index.tsx
new file mode 100644
index 00000000..d595805b
--- /dev/null
+++ b/app/routes/app._index.tsx
@@ -0,0 +1,333 @@
+import { useEffect } from "react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { json } from "@remix-run/node";
+import { useActionData, useNavigation, useSubmit } from "@remix-run/react";
+import {
+ Page,
+ Layout,
+ Text,
+ Card,
+ Button,
+ BlockStack,
+ Box,
+ List,
+ Link,
+ InlineStack,
+} from "@shopify/polaris";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ await authenticate.admin(request);
+
+ return null;
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const { admin } = await authenticate.admin(request);
+ const color = ["Red", "Orange", "Yellow", "Green"][
+ Math.floor(Math.random() * 4)
+ ];
+ const response = await admin.graphql(
+ `#graphql
+ mutation populateProduct($input: ProductInput!) {
+ productCreate(input: $input) {
+ product {
+ id
+ title
+ handle
+ status
+ variants(first: 10) {
+ edges {
+ node {
+ id
+ price
+ barcode
+ createdAt
+ }
+ }
+ }
+ }
+ }
+ }`,
+ {
+ variables: {
+ input: {
+ title: `${color} Snowboard`,
+ },
+ },
+ },
+ );
+ const responseJson = await response.json();
+
+ const variantId =
+ responseJson.data!.productCreate!.product!.variants.edges[0]!.node!.id!;
+ const variantResponse = await admin.graphql(
+ `#graphql
+ mutation shopifyRemixTemplateUpdateVariant($input: ProductVariantInput!) {
+ productVariantUpdate(input: $input) {
+ productVariant {
+ id
+ price
+ barcode
+ createdAt
+ }
+ }
+ }`,
+ {
+ variables: {
+ input: {
+ id: variantId,
+ price: Math.random() * 100,
+ },
+ },
+ },
+ );
+
+ const variantResponseJson = await variantResponse.json();
+
+ return json({
+ product: responseJson!.data!.productCreate!.product,
+ variant: variantResponseJson!.data!.productVariantUpdate!.productVariant,
+ });
+};
+
+export default function Index() {
+ const nav = useNavigation();
+ const actionData = useActionData();
+ const submit = useSubmit();
+ const isLoading =
+ ["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";
+ const productId = actionData?.product?.id.replace(
+ "gid://shopify/Product/",
+ "",
+ );
+
+ useEffect(() => {
+ if (productId) {
+ shopify.toast.show("Product created");
+ }
+ }, [productId]);
+ const generateProduct = () => submit({}, { replace: true, 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.
+
+
+
+
+ {actionData?.product && (
+
+ )}
+
+ {actionData?.product && (
+ <>
+
+ {" "}
+ productCreate mutation
+
+
+
+
+ {JSON.stringify(actionData.product, null, 2)}
+
+
+
+
+ {" "}
+ productVariantUpdate mutation
+
+
+
+
+ {JSON.stringify(actionData.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
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/app.additional.tsx b/app/routes/app.additional.tsx
new file mode 100644
index 00000000..a018c3e2
--- /dev/null
+++ b/app/routes/app.additional.tsx
@@ -0,0 +1,82 @@
+import {
+ Box,
+ Card,
+ Layout,
+ Link,
+ List,
+ Page,
+ Text,
+ BlockStack,
+} from "@shopify/polaris";
+
+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 <ui-nav-menu>
component
+ found in app/routes/app.jsx
.
+
+
+
+
+
+
+
+
+ Resources
+
+
+
+
+ App nav best practices
+
+
+
+
+
+
+
+
+ );
+}
+
+function Code({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/routes/app.tsx b/app/routes/app.tsx
new file mode 100644
index 00000000..702d238e
--- /dev/null
+++ b/app/routes/app.tsx
@@ -0,0 +1,41 @@
+import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
+import { json } 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 polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
+
+import { authenticate } from "../shopify.server";
+
+export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ await authenticate.admin(request);
+
+ return json({ apiKey: process.env.SHOPIFY_API_KEY || "" });
+};
+
+export default function App() {
+ const { apiKey } = useLoaderData();
+
+ return (
+
+
+
+ Home
+
+ Additional page
+
+
+
+ );
+}
+
+// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
+export function ErrorBoundary() {
+ return boundary.error(useRouteError());
+}
+
+export const headers: HeadersFunction = (headersArgs) => {
+ return boundary.headers(headersArgs);
+};
diff --git a/app/routes/auth.$.tsx b/app/routes/auth.$.tsx
new file mode 100644
index 00000000..8919320d
--- /dev/null
+++ b/app/routes/auth.$.tsx
@@ -0,0 +1,8 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ await authenticate.admin(request);
+
+ return null;
+};
diff --git a/app/routes/auth.login/error.server.tsx b/app/routes/auth.login/error.server.tsx
new file mode 100644
index 00000000..2c794974
--- /dev/null
+++ b/app/routes/auth.login/error.server.tsx
@@ -0,0 +1,16 @@
+import type { LoginError } from "@shopify/shopify-app-remix/server";
+import { LoginErrorType } from "@shopify/shopify-app-remix/server";
+
+interface LoginErrorMessage {
+ shop?: string;
+}
+
+export function loginErrorMessage(loginErrors: LoginError): LoginErrorMessage {
+ if (loginErrors?.shop === LoginErrorType.MissingShop) {
+ return { shop: "Please enter your shop domain to log in" };
+ } else if (loginErrors?.shop === LoginErrorType.InvalidShop) {
+ return { shop: "Please enter a valid shop domain to log in" };
+ }
+
+ return {};
+}
diff --git a/app/routes/auth.login/route.tsx b/app/routes/auth.login/route.tsx
new file mode 100644
index 00000000..db0e9025
--- /dev/null
+++ b/app/routes/auth.login/route.tsx
@@ -0,0 +1,69 @@
+import { useState } from "react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { json } 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 { 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 json({ errors, polarisTranslations });
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const errors = loginErrorMessage(await login(request));
+
+ return json({
+ errors,
+ });
+};
+
+export default function Auth() {
+ const loaderData = useLoaderData();
+ const actionData = useActionData();
+ const [shop, setShop] = useState("");
+ const { errors } = actionData || loaderData;
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/webhooks.tsx b/app/routes/webhooks.tsx
new file mode 100644
index 00000000..4099f1a0
--- /dev/null
+++ b/app/routes/webhooks.tsx
@@ -0,0 +1,28 @@
+import type { ActionFunctionArgs } from "@remix-run/node";
+import { authenticate } from "../shopify.server";
+import db from "../db.server";
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const { topic, shop, session, admin } = await authenticate.webhook(request);
+
+ if (!admin) {
+ // The admin context isn't returned if the webhook fired after a shop was uninstalled.
+ throw new Response();
+ }
+
+ switch (topic) {
+ case "APP_UNINSTALLED":
+ if (session) {
+ await db.session.deleteMany({ where: { shop } });
+ }
+
+ break;
+ case "CUSTOMERS_DATA_REQUEST":
+ case "CUSTOMERS_REDACT":
+ case "SHOP_REDACT":
+ default:
+ throw new Response("Unhandled webhook topic", { status: 404 });
+ }
+
+ throw new Response();
+};
diff --git a/app/shopify.server.ts b/app/shopify.server.ts
new file mode 100644
index 00000000..4736a4bc
--- /dev/null
+++ b/app/shopify.server.ts
@@ -0,0 +1,51 @@
+import "@shopify/shopify-app-remix/adapters/node";
+import {
+ ApiVersion,
+ AppDistribution,
+ DeliveryMethod,
+ shopifyApp,
+} from "@shopify/shopify-app-remix/server";
+import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
+import { restResources } from "@shopify/shopify-api/rest/admin/2024-04";
+import prisma from "./db.server";
+
+const shopify = shopifyApp({
+ apiKey: process.env.SHOPIFY_API_KEY,
+ apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
+ apiVersion: ApiVersion.April24,
+ scopes: process.env.SCOPES?.split(","),
+ appUrl: process.env.SHOPIFY_APP_URL || "",
+ authPathPrefix: "/auth",
+ sessionStorage: new PrismaSessionStorage(prisma),
+ distribution: AppDistribution.AppStore,
+ restResources,
+ webhooks: {
+ APP_UNINSTALLED: {
+ deliveryMethod: DeliveryMethod.Http,
+ callbackUrl: "/webhooks",
+ },
+ },
+ hooks: {
+ afterAuth: async ({ session }) => {
+ shopify.registerWebhooks({ session });
+ },
+ },
+ future: {
+ v3_webhookAdminContext: true,
+ v3_authenticatePublic: true,
+ v3_lineItemBilling: true,
+ unstable_newEmbeddedAuthStrategy: true,
+ },
+ ...(process.env.SHOP_CUSTOM_DOMAIN
+ ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
+ : {}),
+});
+
+export default shopify;
+export const apiVersion = ApiVersion.April24;
+export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
+export const authenticate = shopify.authenticate;
+export const unauthenticated = shopify.unauthenticated;
+export const login = shopify.login;
+export const registerWebhooks = shopify.registerWebhooks;
+export const sessionStorage = shopify.sessionStorage;
diff --git a/package.json b/package.json
index c0c86c77..31140537 100644
--- a/package.json
+++ b/package.json
@@ -28,9 +28,9 @@
"@shopify/app": "^3.57.1",
"@shopify/cli": "^3.57.1",
"@shopify/polaris": "^12.0.0",
- "@shopify/shopify-api": "^9.5.1",
- "@shopify/shopify-app-remix": "^2.5.0",
- "@shopify/shopify-app-session-storage-prisma": "^4.0.2",
+ "@shopify/shopify-api": "^10.0.0",
+ "@shopify/shopify-app-remix": "^2.8.2",
+ "@shopify/shopify-app-session-storage-prisma": "^4.0.5",
"isbot": "^5.1.0",
"prisma": "^5.11.0",
"react": "^18.2.0",
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..81402fa8
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,55 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { defineConfig, type UserConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
+// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
+// stop passing in HOST, so we can remove this workaround after the next major release.
+if (
+ process.env.HOST &&
+ (!process.env.SHOPIFY_APP_URL ||
+ process.env.SHOPIFY_APP_URL === process.env.HOST)
+) {
+ process.env.SHOPIFY_APP_URL = process.env.HOST;
+ delete process.env.HOST;
+}
+
+const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
+ .hostname;
+
+let hmrConfig;
+if (host === "localhost") {
+ hmrConfig = {
+ protocol: "ws",
+ host: "localhost",
+ port: 64999,
+ clientPort: 64999,
+ };
+} else {
+ hmrConfig = {
+ protocol: "wss",
+ host: host,
+ port: parseInt(process.env.FRONTEND_PORT!) || 8002,
+ clientPort: 443,
+ };
+}
+
+export default defineConfig({
+ server: {
+ port: Number(process.env.PORT || 3000),
+ hmr: hmrConfig,
+ fs: {
+ // See https://vitejs.dev/config/server-options.html#server-fs-allow for more information
+ allow: ["app", "node_modules"],
+ },
+ },
+ plugins: [
+ remix({
+ ignoredRouteFiles: ["**/.*"],
+ }),
+ tsconfigPaths(),
+ ],
+ build: {
+ assetsInlineLimit: 0,
+ },
+}) satisfies UserConfig;
From 39231bc137e72156624d0163190cf8d22e78c9e2 Mon Sep 17 00:00:00 2001
From: GitHub
Date: Fri, 26 Apr 2024 17:31:15 +0000
Subject: [PATCH 2/2] Convert template to Javascript
---
.graphqlrc.ts | 40 ---
app/db.server.ts | 15 --
app/entry.server.tsx | 58 -----
app/globals.d.ts | 1 -
app/root.tsx | 30 ---
app/routes/_index/route.tsx | 58 -----
app/routes/app._index.jsx | 2 +-
app/routes/app._index.tsx | 333 -------------------------
app/routes/app.additional.tsx | 82 ------
app/routes/app.tsx | 41 ---
app/routes/auth.$.tsx | 8 -
app/routes/auth.login/error.server.tsx | 16 --
app/routes/auth.login/route.tsx | 69 -----
app/routes/webhooks.tsx | 28 ---
app/shopify.server.ts | 51 ----
vite.config.ts | 55 ----
16 files changed, 1 insertion(+), 886 deletions(-)
delete mode 100644 .graphqlrc.ts
delete mode 100644 app/db.server.ts
delete mode 100644 app/entry.server.tsx
delete mode 100644 app/globals.d.ts
delete mode 100644 app/root.tsx
delete mode 100644 app/routes/_index/route.tsx
delete mode 100644 app/routes/app._index.tsx
delete mode 100644 app/routes/app.additional.tsx
delete mode 100644 app/routes/app.tsx
delete mode 100644 app/routes/auth.$.tsx
delete mode 100644 app/routes/auth.login/error.server.tsx
delete mode 100644 app/routes/auth.login/route.tsx
delete mode 100644 app/routes/webhooks.tsx
delete mode 100644 app/shopify.server.ts
delete mode 100644 vite.config.ts
diff --git a/.graphqlrc.ts b/.graphqlrc.ts
deleted file mode 100644
index 23c8d1bc..00000000
--- a/.graphqlrc.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import fs from "fs";
-import { LATEST_API_VERSION } from "@shopify/shopify-api";
-import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
-import type { IGraphQLConfig } from "graphql-config";
-
-function getConfig() {
- const config: IGraphQLConfig = {
- projects: {
- default: shopifyApiProject({
- apiType: ApiType.Admin,
- apiVersion: LATEST_API_VERSION,
- documents: ["./app/**/*.{js,ts,jsx,tsx}"],
- outputDir: "./app/types",
- }),
- },
- };
-
- let extensions: string[] = [];
- try {
- extensions = fs.readdirSync("./extensions");
- } catch {
- // ignore if no extensions
- }
-
- for (const entry of extensions) {
- const extensionPath = `./extensions/${entry}`;
- const schema = `${extensionPath}/schema.graphql`;
- if (!fs.existsSync(schema)) {
- continue;
- }
- config.projects[entry] = {
- schema,
- documents: [`${extensionPath}/**/*.graphql`],
- };
- }
-
- return config;
-}
-
-module.exports = getConfig();
diff --git a/app/db.server.ts b/app/db.server.ts
deleted file mode 100644
index e92f23e6..00000000
--- a/app/db.server.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { PrismaClient } from "@prisma/client";
-
-declare global {
- var prisma: PrismaClient;
-}
-
-const prisma: PrismaClient = global.prisma || new PrismaClient();
-
-if (process.env.NODE_ENV !== "production") {
- if (!global.prisma) {
- global.prisma = new PrismaClient();
- }
-}
-
-export default prisma;
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
deleted file mode 100644
index 07860846..00000000
--- a/app/entry.server.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-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 { isbot } from "isbot";
-import { addDocumentResponseHeaders } from "./shopify.server";
-
-const ABORT_DELAY = 5000;
-
-export default async function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext
-) {
- addDocumentResponseHeaders(request, responseHeaders);
- const userAgent = request.headers.get("user-agent");
- const callbackName = isbot(userAgent ?? '')
- ? "onAllReady"
- : "onShellReady";
-
- return new Promise((resolve, reject) => {
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set("Content-Type", "text/html");
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- );
- pipe(body);
- },
- onShellError(error) {
- reject(error);
- },
- onError(error) {
- responseStatusCode = 500;
- console.error(error);
- },
- }
- );
-
- setTimeout(abort, ABORT_DELAY);
- });
-}
diff --git a/app/globals.d.ts b/app/globals.d.ts
deleted file mode 100644
index cbe652db..00000000
--- a/app/globals.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module "*.css";
diff --git a/app/root.tsx b/app/root.tsx
deleted file mode 100644
index 805f121f..00000000
--- a/app/root.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import {
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
-} from "@remix-run/react";
-
-export default function App() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx
deleted file mode 100644
index cf668d31..00000000
--- a/app/routes/_index/route.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { json, redirect } from "@remix-run/node";
-import { Form, useLoaderData } from "@remix-run/react";
-
-import { login } from "../../shopify.server";
-
-import styles from "./styles.module.css";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- const url = new URL(request.url);
-
- if (url.searchParams.get("shop")) {
- throw redirect(`/app?${url.searchParams.toString()}`);
- }
-
- return json({ showForm: Boolean(login) });
-};
-
-export default function App() {
- const { showForm } = useLoaderData();
-
- return (
-
-
-
A short heading about [your app]
-
- A tagline about [your app] that describes your value proposition.
-
- {showForm && (
-
- )}
-
- -
- Product feature. Some detail about your feature and
- its benefit to your customer.
-
- -
- Product feature. Some detail about your feature and
- its benefit to your customer.
-
- -
- Product feature. Some detail about your feature and
- its benefit to your customer.
-
-
-
-
- );
-}
diff --git a/app/routes/app._index.jsx b/app/routes/app._index.jsx
index 4d803344..6986c891 100644
--- a/app/routes/app._index.jsx
+++ b/app/routes/app._index.jsx
@@ -61,7 +61,7 @@ export const action = async ({ request }) => {
responseJson.data.productCreate.product.variants.edges[0].node.id;
const variantResponse = await admin.graphql(
`#graphql
- mutation updateVariant($input: ProductVariantInput!) {
+ mutation shopifyRemixTemplateUpdateVariant($input: ProductVariantInput!) {
productVariantUpdate(input: $input) {
productVariant {
id
diff --git a/app/routes/app._index.tsx b/app/routes/app._index.tsx
deleted file mode 100644
index d595805b..00000000
--- a/app/routes/app._index.tsx
+++ /dev/null
@@ -1,333 +0,0 @@
-import { useEffect } from "react";
-import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
-import { json } from "@remix-run/node";
-import { useActionData, useNavigation, useSubmit } from "@remix-run/react";
-import {
- Page,
- Layout,
- Text,
- Card,
- Button,
- BlockStack,
- Box,
- List,
- Link,
- InlineStack,
-} from "@shopify/polaris";
-import { authenticate } from "../shopify.server";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- await authenticate.admin(request);
-
- return null;
-};
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const { admin } = await authenticate.admin(request);
- const color = ["Red", "Orange", "Yellow", "Green"][
- Math.floor(Math.random() * 4)
- ];
- const response = await admin.graphql(
- `#graphql
- mutation populateProduct($input: ProductInput!) {
- productCreate(input: $input) {
- product {
- id
- title
- handle
- status
- variants(first: 10) {
- edges {
- node {
- id
- price
- barcode
- createdAt
- }
- }
- }
- }
- }
- }`,
- {
- variables: {
- input: {
- title: `${color} Snowboard`,
- },
- },
- },
- );
- const responseJson = await response.json();
-
- const variantId =
- responseJson.data!.productCreate!.product!.variants.edges[0]!.node!.id!;
- const variantResponse = await admin.graphql(
- `#graphql
- mutation shopifyRemixTemplateUpdateVariant($input: ProductVariantInput!) {
- productVariantUpdate(input: $input) {
- productVariant {
- id
- price
- barcode
- createdAt
- }
- }
- }`,
- {
- variables: {
- input: {
- id: variantId,
- price: Math.random() * 100,
- },
- },
- },
- );
-
- const variantResponseJson = await variantResponse.json();
-
- return json({
- product: responseJson!.data!.productCreate!.product,
- variant: variantResponseJson!.data!.productVariantUpdate!.productVariant,
- });
-};
-
-export default function Index() {
- const nav = useNavigation();
- const actionData = useActionData();
- const submit = useSubmit();
- const isLoading =
- ["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";
- const productId = actionData?.product?.id.replace(
- "gid://shopify/Product/",
- "",
- );
-
- useEffect(() => {
- if (productId) {
- shopify.toast.show("Product created");
- }
- }, [productId]);
- const generateProduct = () => submit({}, { replace: true, 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.
-
-
-
-
- {actionData?.product && (
-
- )}
-
- {actionData?.product && (
- <>
-
- {" "}
- productCreate mutation
-
-
-
-
- {JSON.stringify(actionData.product, null, 2)}
-
-
-
-
- {" "}
- productVariantUpdate mutation
-
-
-
-
- {JSON.stringify(actionData.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
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/routes/app.additional.tsx b/app/routes/app.additional.tsx
deleted file mode 100644
index a018c3e2..00000000
--- a/app/routes/app.additional.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- Box,
- Card,
- Layout,
- Link,
- List,
- Page,
- Text,
- BlockStack,
-} from "@shopify/polaris";
-
-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 <ui-nav-menu>
component
- found in app/routes/app.jsx
.
-
-
-
-
-
-
-
-
- Resources
-
-
-
-
- App nav best practices
-
-
-
-
-
-
-
-
- );
-}
-
-function Code({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/app/routes/app.tsx b/app/routes/app.tsx
deleted file mode 100644
index 702d238e..00000000
--- a/app/routes/app.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
-import { json } 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 polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
-
-import { authenticate } from "../shopify.server";
-
-export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- await authenticate.admin(request);
-
- return json({ apiKey: process.env.SHOPIFY_API_KEY || "" });
-};
-
-export default function App() {
- const { apiKey } = useLoaderData();
-
- return (
-
-
-
- Home
-
- Additional page
-
-
-
- );
-}
-
-// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
-export function ErrorBoundary() {
- return boundary.error(useRouteError());
-}
-
-export const headers: HeadersFunction = (headersArgs) => {
- return boundary.headers(headersArgs);
-};
diff --git a/app/routes/auth.$.tsx b/app/routes/auth.$.tsx
deleted file mode 100644
index 8919320d..00000000
--- a/app/routes/auth.$.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { authenticate } from "../shopify.server";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- await authenticate.admin(request);
-
- return null;
-};
diff --git a/app/routes/auth.login/error.server.tsx b/app/routes/auth.login/error.server.tsx
deleted file mode 100644
index 2c794974..00000000
--- a/app/routes/auth.login/error.server.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { LoginError } from "@shopify/shopify-app-remix/server";
-import { LoginErrorType } from "@shopify/shopify-app-remix/server";
-
-interface LoginErrorMessage {
- shop?: string;
-}
-
-export function loginErrorMessage(loginErrors: LoginError): LoginErrorMessage {
- if (loginErrors?.shop === LoginErrorType.MissingShop) {
- return { shop: "Please enter your shop domain to log in" };
- } else if (loginErrors?.shop === LoginErrorType.InvalidShop) {
- return { shop: "Please enter a valid shop domain to log in" };
- }
-
- return {};
-}
diff --git a/app/routes/auth.login/route.tsx b/app/routes/auth.login/route.tsx
deleted file mode 100644
index db0e9025..00000000
--- a/app/routes/auth.login/route.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useState } from "react";
-import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
-import { json } 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 { 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 json({ errors, polarisTranslations });
-};
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const errors = loginErrorMessage(await login(request));
-
- return json({
- errors,
- });
-};
-
-export default function Auth() {
- const loaderData = useLoaderData();
- const actionData = useActionData();
- const [shop, setShop] = useState("");
- const { errors } = actionData || loaderData;
-
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/app/routes/webhooks.tsx b/app/routes/webhooks.tsx
deleted file mode 100644
index 4099f1a0..00000000
--- a/app/routes/webhooks.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { authenticate } from "../shopify.server";
-import db from "../db.server";
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const { topic, shop, session, admin } = await authenticate.webhook(request);
-
- if (!admin) {
- // The admin context isn't returned if the webhook fired after a shop was uninstalled.
- throw new Response();
- }
-
- switch (topic) {
- case "APP_UNINSTALLED":
- if (session) {
- await db.session.deleteMany({ where: { shop } });
- }
-
- break;
- case "CUSTOMERS_DATA_REQUEST":
- case "CUSTOMERS_REDACT":
- case "SHOP_REDACT":
- default:
- throw new Response("Unhandled webhook topic", { status: 404 });
- }
-
- throw new Response();
-};
diff --git a/app/shopify.server.ts b/app/shopify.server.ts
deleted file mode 100644
index 4736a4bc..00000000
--- a/app/shopify.server.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import "@shopify/shopify-app-remix/adapters/node";
-import {
- ApiVersion,
- AppDistribution,
- DeliveryMethod,
- shopifyApp,
-} from "@shopify/shopify-app-remix/server";
-import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
-import { restResources } from "@shopify/shopify-api/rest/admin/2024-04";
-import prisma from "./db.server";
-
-const shopify = shopifyApp({
- apiKey: process.env.SHOPIFY_API_KEY,
- apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
- apiVersion: ApiVersion.April24,
- scopes: process.env.SCOPES?.split(","),
- appUrl: process.env.SHOPIFY_APP_URL || "",
- authPathPrefix: "/auth",
- sessionStorage: new PrismaSessionStorage(prisma),
- distribution: AppDistribution.AppStore,
- restResources,
- webhooks: {
- APP_UNINSTALLED: {
- deliveryMethod: DeliveryMethod.Http,
- callbackUrl: "/webhooks",
- },
- },
- hooks: {
- afterAuth: async ({ session }) => {
- shopify.registerWebhooks({ session });
- },
- },
- future: {
- v3_webhookAdminContext: true,
- v3_authenticatePublic: true,
- v3_lineItemBilling: true,
- unstable_newEmbeddedAuthStrategy: true,
- },
- ...(process.env.SHOP_CUSTOM_DOMAIN
- ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
- : {}),
-});
-
-export default shopify;
-export const apiVersion = ApiVersion.April24;
-export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
-export const authenticate = shopify.authenticate;
-export const unauthenticated = shopify.unauthenticated;
-export const login = shopify.login;
-export const registerWebhooks = shopify.registerWebhooks;
-export const sessionStorage = shopify.sessionStorage;
diff --git a/vite.config.ts b/vite.config.ts
deleted file mode 100644
index 81402fa8..00000000
--- a/vite.config.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { vitePlugin as remix } from "@remix-run/dev";
-import { defineConfig, type UserConfig } from "vite";
-import tsconfigPaths from "vite-tsconfig-paths";
-
-// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
-// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
-// stop passing in HOST, so we can remove this workaround after the next major release.
-if (
- process.env.HOST &&
- (!process.env.SHOPIFY_APP_URL ||
- process.env.SHOPIFY_APP_URL === process.env.HOST)
-) {
- process.env.SHOPIFY_APP_URL = process.env.HOST;
- delete process.env.HOST;
-}
-
-const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
- .hostname;
-
-let hmrConfig;
-if (host === "localhost") {
- hmrConfig = {
- protocol: "ws",
- host: "localhost",
- port: 64999,
- clientPort: 64999,
- };
-} else {
- hmrConfig = {
- protocol: "wss",
- host: host,
- port: parseInt(process.env.FRONTEND_PORT!) || 8002,
- clientPort: 443,
- };
-}
-
-export default defineConfig({
- server: {
- port: Number(process.env.PORT || 3000),
- hmr: hmrConfig,
- fs: {
- // See https://vitejs.dev/config/server-options.html#server-fs-allow for more information
- allow: ["app", "node_modules"],
- },
- },
- plugins: [
- remix({
- ignoredRouteFiles: ["**/.*"],
- }),
- tsconfigPaths(),
- ],
- build: {
- assetsInlineLimit: 0,
- },
-}) satisfies UserConfig;