diff --git a/.graphqlrc.ts b/.graphqlrc.ts deleted file mode 100644 index ad9c5f81..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}", "./app/.server/**/*.{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 bafa6cc2..00000000 --- a/app/db.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -declare global { - var prisma: PrismaClient; -} - -if (process.env.NODE_ENV !== "production") { - if (!global.prisma) { - global.prisma = new PrismaClient(); - } -} - -const prisma: PrismaClient = global.prisma || new PrismaClient(); - -export default prisma; diff --git a/app/entry.server.tsx b/app/entry.server.tsx deleted file mode 100644 index 86274311..00000000 --- a/app/entry.server.tsx +++ /dev/null @@ -1,59 +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"; - -export const streamTimeout = 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); - }, - } - ); - - // Automatically timeout the React renderer after 6 seconds, which ensures - // React has enough time to flush down the rejected boundary contents - setTimeout(abort, streamTimeout + 1000); - }); -} 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.ts b/app/routes.ts deleted file mode 100644 index 83892841..00000000 --- a/app/routes.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { flatRoutes } from "@remix-run/fs-routes"; - -export default flatRoutes(); diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx deleted file mode 100644 index 2de9dd47..00000000 --- a/app/routes/_index/route.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { LoaderFunctionArgs } from "@remix-run/node"; -import { 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 { 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 deleted file mode 100644 index 18b215b0..00000000 --- a/app/routes/app._index.tsx +++ /dev/null @@ -1,334 +0,0 @@ -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 { TitleBar, useAppBridge } from "@shopify/app-bridge-react"; -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($product: ProductCreateInput!) { - productCreate(product: $product) { - product { - id - title - handle - status - variants(first: 10) { - edges { - node { - id - price - barcode - createdAt - } - } - } - } - } - }`, - { - variables: { - product: { - title: `${color} Snowboard`, - }, - }, - }, - ); - const responseJson = await response.json(); - - const product = responseJson.data!.productCreate!.product!; - const variantId = product.variants.edges[0]!.node!.id!; - - const variantResponse = await admin.graphql( - `#graphql - mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { - productVariantsBulkUpdate(productId: $productId, variants: $variants) { - productVariants { - id - price - barcode - createdAt - } - } - }`, - { - variables: { - productId: product.id, - variants: [{ id: variantId, price: "100.00" }], - }, - }, - ); - - const variantResponseJson = await variantResponse.json(); - - return { - product: responseJson!.data!.productCreate!.product, - variant: - variantResponseJson!.data!.productVariantsBulkUpdate!.productVariants, - }; -}; - -export default function Index() { - const fetcher = useFetcher(); - - const shopify = useAppBridge(); - const isLoading = - ["loading", "submitting"].includes(fetcher.state) && - fetcher.formMethod === "POST"; - const productId = fetcher.data?.product?.id.replace( - "gid://shopify/Product/", - "", - ); - - useEffect(() => { - if (productId) { - shopify.toast.show("Product created"); - } - }, [productId, shopify]); - 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 - - - - - - - -
-
-
- ); -} diff --git a/app/routes/app.additional.tsx b/app/routes/app.additional.tsx deleted file mode 100644 index eb9b0cfd..00000000 --- a/app/routes/app.additional.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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} - - ); -} diff --git a/app/routes/app.tsx b/app/routes/app.tsx deleted file mode 100644 index bdcf1162..00000000 --- a/app/routes/app.tsx +++ /dev/null @@ -1,41 +0,0 @@ -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 { NavMenu } from "@shopify/app-bridge-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 { 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 0e9aece7..00000000 --- a/app/routes/auth.login/route.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 { 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 }; -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - const errors = loginErrorMessage(await login(request)); - - return { - errors, - }; -}; - -export default function Auth() { - const loaderData = useLoaderData(); - const actionData = useActionData(); - const [shop, setShop] = useState(""); - const { errors } = actionData || loaderData; - - return ( - - - -
- - - Log in - - - - -
-
-
-
- ); -} diff --git a/app/routes/webhooks.app.uninstalled.tsx b/app/routes/webhooks.app.uninstalled.tsx deleted file mode 100644 index 54d3161c..00000000 --- a/app/routes/webhooks.app.uninstalled.tsx +++ /dev/null @@ -1,17 +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 { shop, session, topic } = await authenticate.webhook(request); - - console.log(`Received ${topic} webhook for ${shop}`); - - // Webhook requests can trigger multiple times and after an app has already been uninstalled. - // If this webhook already ran, the session may have been deleted previously. - if (session) { - await db.session.deleteMany({ where: { shop } }); - } - - return new Response(); -}; diff --git a/app/shopify.server.js b/app/shopify.server.js index ab03ab11..ec980711 100644 --- a/app/shopify.server.js +++ b/app/shopify.server.js @@ -5,7 +5,6 @@ import { 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-10"; import prisma from "./db.server"; const shopify = shopifyApp({ @@ -17,9 +16,9 @@ const shopify = shopifyApp({ authPathPrefix: "/auth", sessionStorage: new PrismaSessionStorage(prisma), distribution: AppDistribution.AppStore, - restResources, future: { unstable_newEmbeddedAuthStrategy: true, + removeRest: true, }, ...(process.env.SHOP_CUSTOM_DOMAIN ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] } diff --git a/app/shopify.server.ts b/app/shopify.server.ts deleted file mode 100644 index ec980711..00000000 --- a/app/shopify.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import "@shopify/shopify-app-remix/adapters/node"; -import { - ApiVersion, - AppDistribution, - shopifyApp, -} from "@shopify/shopify-app-remix/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, - scopes: process.env.SCOPES?.split(","), - appUrl: process.env.SHOPIFY_APP_URL || "", - authPathPrefix: "/auth", - sessionStorage: new PrismaSessionStorage(prisma), - distribution: AppDistribution.AppStore, - future: { - unstable_newEmbeddedAuthStrategy: true, - removeRest: true, - }, - ...(process.env.SHOP_CUSTOM_DOMAIN - ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] } - : {}), -}); - -export default shopify; -export const apiVersion = ApiVersion.October24; -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.js b/vite.config.js index 204a574d..16fd5997 100644 --- a/vite.config.js +++ b/vite.config.js @@ -54,7 +54,7 @@ export default defineConfig({ v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, - v3_singleFetch: true, + v3_singleFetch: false, v3_routeConfig: true, }, }), diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 82142f42..00000000 --- a/vite.config.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { vitePlugin as remix } from "@remix-run/dev"; -import { installGlobals } from "@remix-run/node"; -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. -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: ["**/.*"], - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_lazyRouteDiscovery: true, - v3_singleFetch: false, - v3_routeConfig: true, - }, - }), - tsconfigPaths(), - ], - build: { - assetsInlineLimit: 0, - }, -}) satisfies UserConfig;