-
A short heading about [your app]
-
A tagline about [your app] that describes your value proposition.
+
+
+
A short heading about [your app]
+
+ A tagline about [your app] that describes your value proposition.
+
{showForm && (
-
);
-}
+}
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/_index/style.css b/sample-apps/payment-customizations/app/routes/_index/styles.module.css
similarity index 80%
rename from sample-apps/payment-customizations/app/routes/_index/style.css
rename to sample-apps/payment-customizations/app/routes/_index/styles.module.css
index b8d93c4b..271721fd 100644
--- a/sample-apps/payment-customizations/app/routes/_index/style.css
+++ b/sample-apps/payment-customizations/app/routes/_index/styles.module.css
@@ -1,11 +1,3 @@
-html,
-body {
- height: 100%;
- width: 100%;
- font-family: "Roboto", sans-serif;
- line-height: 1.6;
-}
-
.index {
align-items: center;
display: flex;
@@ -16,13 +8,13 @@ body {
padding: 1rem;
}
-h1,
-p {
+.heading,
+.text {
padding: 0;
margin: 0;
}
-p {
+.text {
font-size: 1.2rem;
padding-bottom: 2rem;
}
@@ -32,7 +24,7 @@ p {
gap: 2rem;
}
-form {
+.form {
display: flex;
align-items: center;
justify-content: flex-start;
@@ -40,7 +32,7 @@ form {
gap: 1rem;
}
-label {
+.label {
display: grid;
gap: 0.2rem;
max-width: 20rem;
@@ -48,15 +40,15 @@ label {
font-size: 1rem;
}
-input[type="text"] {
+.input {
padding: 0.4rem;
}
-button {
+.button {
padding: 0.4rem;
}
-ul {
+.list {
list-style: none;
padding: 0;
padding-top: 3rem;
@@ -65,17 +57,17 @@ ul {
gap: 2rem;
}
-li {
+.list > li {
max-width: 20rem;
text-align: left;
}
@media only screen and (max-width: 50rem) {
- ul {
+ .list {
display: block;
}
- li {
+ .list > li {
padding-bottom: 1rem;
}
}
diff --git a/sample-apps/payment-customizations/app/routes/app._index.jsx b/sample-apps/payment-customizations/app/routes/app._index.jsx
index d38e08a9..660e1291 100644
--- a/sample-apps/payment-customizations/app/routes/app._index.jsx
+++ b/sample-apps/payment-customizations/app/routes/app._index.jsx
@@ -1,43 +1,24 @@
import { useEffect } from "react";
-import { json } from "@remix-run/node";
-import {
- Link,
- useActionData,
- useLoaderData,
- useNavigation,
- useSubmit,
-} from "@remix-run/react";
-import {
- Page,
- Layout,
- Text,
- VerticalStack,
- Card,
- Button,
- HorizontalStack,
- Box,
- Divider,
- List,
-} from "@shopify/polaris";
-
-import { authenticate } from "../shopify.server";
+import { useFetcher } from "react-router";
+import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
+import { authenticate } from "../shopify.server.js";
+import { boundary } from "@shopify/shopify-app-react-router/server";
export const loader = async ({ request }) => {
- const { session } = await authenticate.admin(request);
+ await authenticate.admin(request);
- return json({ shop: session.shop.replace(".myshopify.com", "") });
+ return null;
};
-export async function action({ request }) {
+export const action = async ({ request }) => {
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) {
+ mutation populateProduct($product: ProductCreateInput!) {
+ productCreate(product: $product) {
product {
id
title
@@ -58,220 +39,210 @@ export async function action({ request }) {
}`,
{
variables: {
- input: {
+ product: {
title: `${color} Snowboard`,
- variants: [{ price: Math.random() * 100 }],
},
},
- }
+ },
);
-
const responseJson = await response.json();
- return json({
+ const product = responseJson.data.productCreate.product;
+ const variantId = product.variants.edges[0].node.id;
+
+ const variantResponse = await admin.graphql(
+ `#graphql
+ mutation shopifyReactRouterTemplateUpdateVariant($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 nav = useNavigation();
- const { shop } = useLoaderData();
- const actionData = useActionData();
- const submit = useSubmit();
+ const fetcher = useFetcher();
+ const shopify = useAppBridge();
const isLoading =
- ["loading", "submitting"].includes(nav.state) && nav.formMethod === "POST";
-
- const productId = actionData?.product?.id.replace(
+ ["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]);
-
- const generateProduct = () => submit({}, { replace: true, method: "POST" });
+ }, [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.
-
-
-
- {actionData?.product && (
-
- )}
-
-
- {actionData?.product && (
-
-
- {JSON.stringify(actionData.product, 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 = (headersArgs) => {
+ return boundary.headers(headersArgs);
+};
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/app.additional.jsx b/sample-apps/payment-customizations/app/routes/app.additional.jsx
index 77c1fed9..5944604c 100644
--- a/sample-apps/payment-customizations/app/routes/app.additional.jsx
+++ b/sample-apps/payment-customizations/app/routes/app.additional.jsx
@@ -1,80 +1,40 @@
-import { Link } from "@remix-run/react";
-import {
- Box,
- Card,
- Layout,
- List,
- Page,
- Text,
- VerticalStack,
-} 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 <ui-nav-menu>
component
- found in app/routes/app.jsx
.
-
-
-
-
-
-
-
-
- Resources
-
-
-
-
- App nav best practices
-
-
-
-
-
-
-
-
+
+
+
+
+ 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
+
+
+
+
+
);
-}
-
-function Code({ children }) {
- return (
-
- {children}
-
- );
-}
+}
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/app.jsx b/sample-apps/payment-customizations/app/routes/app.jsx
index d7867b17..55bf6470 100644
--- a/sample-apps/payment-customizations/app/routes/app.jsx
+++ b/sample-apps/payment-customizations/app/routes/app.jsx
@@ -1,48 +1,37 @@
-import { json } from "@remix-run/node";
-import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
-import { AppProvider as PolarisAppProvider } from "@shopify/polaris";
-import polarisStyles from "@shopify/polaris/build/esm/styles.css";
-import { boundary } from "@shopify/shopify-app-remix";
+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 { AppProvider } from "@shopify/shopify-app-react-router/react";
-import { authenticate } from "../shopify.server";
+import { authenticate } from "../shopify.server.js";
-export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
-
-export async function loader({ request }) {
+export const loader = async ({ request }) => {
await authenticate.admin(request);
- return json({
- polarisTranslations: require(`@shopify/polaris/locales/en.json`),
- apiKey: process.env.SHOPIFY_API_KEY,
- });
-}
+ return { apiKey: process.env.SHOPIFY_API_KEY || "" };
+};
export default function App() {
- const { polarisTranslations } = useLoaderData();
const { apiKey } = useLoaderData();
return (
- <>
-
-
- Home
+
+
+
+ Home
+
Additional page
-
-
-
-
- >
+
+
+
);
}
-// 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());
}
export const headers = (headersArgs) => {
return boundary.headers(headersArgs);
-};
+};
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/app.payment-customization.$functionId.$id.jsx b/sample-apps/payment-customizations/app/routes/app.payment-customization.$functionId.$id.jsx
index 936b0a73..5eed45dd 100644
--- a/sample-apps/payment-customizations/app/routes/app.payment-customization.$functionId.$id.jsx
+++ b/sample-apps/payment-customizations/app/routes/app.payment-customization.$functionId.$id.jsx
@@ -1,21 +1,6 @@
import { useState, useEffect } from "react";
-import {
- Banner,
- Button,
- Card,
- FormLayout,
- Layout,
- Page,
- TextField,
-} from "@shopify/polaris";
-import {
- Form,
- useActionData,
- useNavigation,
- useSubmit,
- useLoaderData,
-} from "@remix-run/react";
-import { json } from "@remix-run/node";
+import { useActionData, useNavigation, useSubmit, useLoaderData } from "react-router";
+import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
@@ -54,10 +39,10 @@ export const loader = async ({ params, request }) => {
responseJson.data.paymentCustomization?.metafield?.value &&
JSON.parse(responseJson.data.paymentCustomization.metafield.value);
- return json({
+ return {
paymentMethodName: metafield?.paymentMethodName ?? "",
cartTotal: metafield?.cartTotal ?? "0",
- });
+ };
};
// This is a server-side action that is invoked when the form is submitted.
@@ -111,7 +96,7 @@ export const action = async ({ params, request }) => {
const responseJson = await response.json();
const errors = responseJson.data.paymentCustomizationCreate?.userErrors;
- return json({ errors });
+ return { errors };
} else {
const response = await admin.graphql(
`#graphql
@@ -136,10 +121,15 @@ export const action = async ({ params, request }) => {
const responseJson = await response.json();
const errors = responseJson.data.paymentCustomizationUpdate?.userErrors;
- return json({ errors });
+ return { errors };
}
};
+// Required for single fetch compatibility
+export const headers = (headersArgs) => {
+ return boundary.headers(headersArgs);
+};
+
// This is the client-side component that renders the form.
export default function PaymentCustomization() {
const submit = useSubmit();
@@ -154,24 +144,25 @@ export default function PaymentCustomization() {
const isLoading = navigation.state === "submitting";
const errorBanner = actionData?.errors.length ? (
-
-
-
- {actionData?.errors.map((error, index) => {
- return - {error.message}
;
- })}
-
-
-
+
+
+ {actionData?.errors.map((error, index) => {
+ return - {error.message}
;
+ })}
+
+
) : null;
- const handleSubmit = () => {
+ const handleSubmit = (event) => {
+ event.preventDefault();
submit({ paymentMethodName, cartTotal }, { method: "post" });
};
+ const handleReset = () => {
+ setPaymentMethodName(loaderData.paymentMethodName);
+ setCartTotal(loaderData.cartTotal);
+ };
+
useEffect(() => {
if (actionData?.errors.length === 0) {
open("shopify:admin/settings/payments/customizations", "_top");
@@ -179,52 +170,42 @@ export default function PaymentCustomization() {
}, [actionData?.errors]);
return (
-
- open("shopify:admin/settings/payments/customizations", "_top"),
- }}
- primaryAction={{
- content: "Save",
- loading: isLoading,
- onAction: handleSubmit,
- }}
- >
-
+
-
+
+
+
+ setPaymentMethodName(e.target.value)}
+ disabled={isLoading}
+ autoComplete="on"
+ required
+ >
+
+ setCartTotal(e.target.value)}
+ disabled={isLoading}
+ min="0"
+ step="0.01"
+ required
+ >
+
+
+
+
);
}
diff --git a/sample-apps/payment-customizations/app/routes/auth.$.jsx b/sample-apps/payment-customizations/app/routes/auth.$.jsx
index fab52239..e6e4596e 100644
--- a/sample-apps/payment-customizations/app/routes/auth.$.jsx
+++ b/sample-apps/payment-customizations/app/routes/auth.$.jsx
@@ -1,7 +1,12 @@
-import { authenticate } from "../shopify.server";
+import { authenticate } from "../shopify.server.js";
+import { boundary } from "@shopify/shopify-app-react-router/server";
-export async function loader({ request }) {
+export const loader = async ({ request }) => {
await authenticate.admin(request);
return null;
-}
+};
+
+export const headers = (headersArgs) => {
+ return boundary.headers(headersArgs);
+};
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/auth.login/error.server.jsx b/sample-apps/payment-customizations/app/routes/auth.login/error.server.js
similarity index 80%
rename from sample-apps/payment-customizations/app/routes/auth.login/error.server.jsx
rename to sample-apps/payment-customizations/app/routes/auth.login/error.server.js
index abe43928..eeaf9365 100644
--- a/sample-apps/payment-customizations/app/routes/auth.login/error.server.jsx
+++ b/sample-apps/payment-customizations/app/routes/auth.login/error.server.js
@@ -1,4 +1,4 @@
-import { LoginErrorType } from "@shopify/shopify-app-remix";
+import { LoginErrorType } from "@shopify/shopify-app-react-router/server";
export function loginErrorMessage(loginErrors) {
if (loginErrors?.shop === LoginErrorType.MissingShop) {
@@ -8,4 +8,4 @@ export function loginErrorMessage(loginErrors) {
}
return {};
-}
+}
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/auth.login/route.jsx b/sample-apps/payment-customizations/app/routes/auth.login/route.jsx
index bc494970..43c2d068 100644
--- a/sample-apps/payment-customizations/app/routes/auth.login/route.jsx
+++ b/sample-apps/payment-customizations/app/routes/auth.login/route.jsx
@@ -1,71 +1,49 @@
import { useState } from "react";
-import { json } from "@remix-run/node";
-import {
- AppProvider as PolarisAppProvider,
- Button,
- Card,
- FormLayout,
- Page,
- Text,
- TextField,
-} from "@shopify/polaris";
+import { Form, useActionData, useLoaderData } from "react-router";
+import { AppProvider } from "@shopify/shopify-app-react-router/react";
-import { Form, useActionData, useLoaderData } from "@remix-run/react";
-import polarisStyles from "@shopify/polaris/build/esm/styles.css";
+import { login } from "../../shopify.server.js";
+import { loginErrorMessage } from "./error.server.js";
-import { login } from "../../shopify.server";
-import { loginErrorMessage } from "./error.server";
-
-export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
-
-export async function loader({ request }) {
+export const loader = async ({ request }) => {
const errors = loginErrorMessage(await login(request));
- return json({
- errors,
- polarisTranslations: require(`@shopify/polaris/locales/en.json`),
- });
-}
+ return { errors };
+};
-export async function action({ request }) {
+export const action = async ({ request }) => {
const errors = loginErrorMessage(await login(request));
- return json({
+ return {
errors,
- });
-}
+ };
+};
export default function Auth() {
- const { polarisTranslations } = useLoaderData();
const loaderData = useLoaderData();
const actionData = useActionData();
const [shop, setShop] = useState("");
const { errors } = actionData || loaderData;
return (
-
-
-
-
-
-
- Log in
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Log in
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/webhooks.app.scopes_update.jsx b/sample-apps/payment-customizations/app/routes/webhooks.app.scopes_update.jsx
new file mode 100644
index 00000000..47f64c5c
--- /dev/null
+++ b/sample-apps/payment-customizations/app/routes/webhooks.app.scopes_update.jsx
@@ -0,0 +1,11 @@
+import { authenticate } from "../shopify.server.js";
+
+export const action = async ({ request }) => {
+ const { topic } = await authenticate.webhook(request);
+
+ if (topic === "APP_SCOPES_UPDATE") {
+ console.log("Received APP_SCOPES_UPDATE webhook. Update scopes.");
+ }
+
+ return new Response();
+};
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/webhooks.app.uninstalled.jsx b/sample-apps/payment-customizations/app/routes/webhooks.app.uninstalled.jsx
new file mode 100644
index 00000000..6418c696
--- /dev/null
+++ b/sample-apps/payment-customizations/app/routes/webhooks.app.uninstalled.jsx
@@ -0,0 +1,16 @@
+import { authenticate } from "../shopify.server.js";
+import db from "../db.server.js";
+
+export const action = async ({ request }) => {
+ 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();
+};
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/app/routes/webhooks.jsx b/sample-apps/payment-customizations/app/routes/webhooks.jsx
deleted file mode 100644
index 8250212d..00000000
--- a/sample-apps/payment-customizations/app/routes/webhooks.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { authenticate } from "../shopify.server";
-import db from "../db.server";
-
-export const action = async ({ request }) => {
- const { topic, shop } = await authenticate.webhook(request);
-
- switch (topic) {
- case "APP_UNINSTALLED":
- 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/sample-apps/payment-customizations/app/shopify.server.js b/sample-apps/payment-customizations/app/shopify.server.js
index ee7eb5d3..9b05a558 100644
--- a/sample-apps/payment-customizations/app/shopify.server.js
+++ b/sample-apps/payment-customizations/app/shopify.server.js
@@ -1,35 +1,24 @@
-import "@shopify/shopify-app-remix/adapters/node";
+import "@shopify/shopify-app-react-router/adapters/node";
import {
+ ApiVersion,
AppDistribution,
- DeliveryMethod,
shopifyApp,
- LATEST_API_VERSION,
-} from "@shopify/shopify-app-remix";
+} from "@shopify/shopify-app-react-router/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
-import { restResources } from "@shopify/shopify-api/rest/admin/2023-07";
-
-import prisma from "./db.server";
+import prisma from "./db.server.js";
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
- apiVersion: LATEST_API_VERSION,
+ apiVersion: ApiVersion.January25,
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: {
+ unstable_newEmbeddedAuthStrategy: true,
+ removeRest: true,
},
...(process.env.SHOP_CUSTOM_DOMAIN
? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
@@ -37,9 +26,10 @@ const shopify = shopifyApp({
});
export default shopify;
-export const apiVersion = LATEST_API_VERSION;
+export const apiVersion = ApiVersion.January25;
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;
+export const sessionStorage = shopify.sessionStorage;
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/extensions/payment-customization-js/shopify.extension.toml b/sample-apps/payment-customizations/extensions/payment-customization-js/shopify.extension.toml
index e83cc2a5..a1ca19e0 100644
--- a/sample-apps/payment-customizations/extensions/payment-customization-js/shopify.extension.toml
+++ b/sample-apps/payment-customizations/extensions/payment-customization-js/shopify.extension.toml
@@ -1,4 +1,4 @@
-api_version = "2024-04"
+api_version = "2025-04"
[[extensions]]
handle = "payment-customization-js"
diff --git a/sample-apps/payment-customizations/extensions/payment-customization-rust/shopify.extension.toml b/sample-apps/payment-customizations/extensions/payment-customization-rust/shopify.extension.toml
index f71f0579..85fffd92 100644
--- a/sample-apps/payment-customizations/extensions/payment-customization-rust/shopify.extension.toml
+++ b/sample-apps/payment-customizations/extensions/payment-customization-rust/shopify.extension.toml
@@ -1,4 +1,4 @@
-api_version = "2024-04"
+api_version = "2025-04"
[[extensions]]
handle = "payment-customization-rust"
diff --git a/sample-apps/payment-customizations/package.json b/sample-apps/payment-customizations/package.json
index 8de80e79..a758a5e6 100644
--- a/sample-apps/payment-customizations/package.json
+++ b/sample-apps/payment-customizations/package.json
@@ -1,51 +1,70 @@
{
- "name": "payment-customizations",
+ "name": "app",
"private": true,
"scripts": {
- "build": "NODE_ENV=production remix build",
- "predev": "prisma generate && prisma migrate deploy",
+ "build": "react-router build",
"dev": "shopify app dev",
"config:link": "shopify app config link",
- "config:push": "shopify app config push",
"generate": "shopify app generate",
"deploy": "shopify app deploy",
"config:use": "shopify app config use",
"env": "shopify app env",
- "start": "remix-serve build",
- "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
+ "start": "react-router-serve ./build/server/index.js",
+ "docker-start": "npm run setup && npm run start",
+ "setup": "prisma generate && prisma migrate deploy",
+ "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"shopify": "shopify",
"prisma": "prisma",
- "setup": "prisma generate && prisma migrate deploy"
+ "graphql-codegen": "graphql-codegen",
+ "vite": "vite"
+ },
+ "type": "module",
+ "engines": {
+ "node": ">=20.10"
},
"dependencies": {
- "@prisma/client": "^4.13.0",
- "@remix-run/node": "^1.19.0",
- "@remix-run/react": "^1.19.0",
- "@remix-run/serve": "^1.19.0",
- "@shopify/app": "^3.48.0",
- "@shopify/app-bridge-types": "^0.0.2",
- "@shopify/cli": "^3.48.0",
- "@shopify/polaris": "^11.1.2",
- "@shopify/shopify-app-remix": "^1.0.0",
- "@shopify/shopify-app-session-storage-prisma": "^1.0.0",
- "cross-env": "^7.0.3",
- "isbot": "^4.1.0",
- "prisma": "^4.13.0",
+ "@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/shopify-app-react-router": "^0.1.0",
+ "@shopify/shopify-app-session-storage-prisma": "^6.0.0",
+ "isbot": "^5.1.0",
+ "prisma": "^6.2.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-router": "^7.0.0"
},
"devDependencies": {
- "@remix-run/dev": "^1.19.0",
- "@remix-run/eslint-config": "^1.19.0",
- "@types/eslint": "^8.40.0",
- "eslint": "^8.42.0",
- "eslint-config-prettier": "^8.8.0",
- "prettier": "^2.8.8"
+ "@shopify/api-codegen-preset": "^1.1.1",
+ "eslint": "^8.38.0",
+ "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",
+ "vite": "^6.2.2"
+ },
+ "workspaces": {
+ "packages": [
+ "extensions/*"
+ ]
},
- "workspaces": [
- "web",
- "web/frontend",
- "extensions/*"
+ "trustedDependencies": [
+ "@shopify/plugin-cloudflare"
],
- "author": "andrewhassan"
+ "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/payment-customizations/prisma/migrations/20230615155147_create_session_table/migration.sql b/sample-apps/payment-customizations/prisma/migrations/20240530213853_create_session_table/migration.sql
similarity index 51%
rename from sample-apps/payment-customizations/prisma/migrations/20230615155147_create_session_table/migration.sql
rename to sample-apps/payment-customizations/prisma/migrations/20240530213853_create_session_table/migration.sql
index 5decb97b..1b3f1247 100644
--- a/sample-apps/payment-customizations/prisma/migrations/20230615155147_create_session_table/migration.sql
+++ b/sample-apps/payment-customizations/prisma/migrations/20240530213853_create_session_table/migration.sql
@@ -7,5 +7,12 @@ CREATE TABLE "Session" (
"scope" TEXT,
"expires" DATETIME,
"accessToken" TEXT NOT NULL,
- "userId" BIGINT
+ "userId" BIGINT,
+ "firstName" TEXT,
+ "lastName" TEXT,
+ "email" TEXT,
+ "accountOwner" BOOLEAN NOT NULL DEFAULT false,
+ "locale" TEXT,
+ "collaborator" BOOLEAN DEFAULT false,
+ "emailVerified" BOOLEAN DEFAULT false
);
diff --git a/sample-apps/payment-customizations/prisma/schema.prisma b/sample-apps/payment-customizations/prisma/schema.prisma
index ac90b788..fdbf8195 100644
--- a/sample-apps/payment-customizations/prisma/schema.prisma
+++ b/sample-apps/payment-customizations/prisma/schema.prisma
@@ -5,18 +5,28 @@ generator client {
provider = "prisma-client-js"
}
+// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
+// enough when changing adapters.
+// See https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string for more information
datasource db {
provider = "sqlite"
url = "file:dev.sqlite"
}
model Session {
- id String @id
- shop String
- state String
- isOnline Boolean @default(false)
- scope String?
- expires DateTime?
- accessToken String
- userId BigInt?
+ id String @id
+ shop String
+ state String
+ isOnline Boolean @default(false)
+ scope String?
+ expires DateTime?
+ accessToken String
+ userId BigInt?
+ firstName String?
+ lastName String?
+ email String?
+ accountOwner Boolean @default(false)
+ locale String?
+ collaborator Boolean? @default(false)
+ emailVerified Boolean? @default(false)
}
diff --git a/sample-apps/payment-customizations/react-router.config.js b/sample-apps/payment-customizations/react-router.config.js
new file mode 100644
index 00000000..37e54da1
--- /dev/null
+++ b/sample-apps/payment-customizations/react-router.config.js
@@ -0,0 +1,3 @@
+export default {
+ ssr: true,
+};
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/remix.config.js b/sample-apps/payment-customizations/remix.config.js
deleted file mode 100644
index 57614909..00000000
--- a/sample-apps/payment-customizations/remix.config.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// eslint-disable-next-line no-warning-comments
-// TODO: Document this properly somewhere. Where? We should discuss this issue with the CLI team.
-// 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;
-}
-
-/** @type {import('@remix-run/dev').AppConfig} */
-module.exports = {
- ignoredRouteFiles: ["**/.*"],
- appDirectory: "app",
- serverModuleFormat: "cjs",
- future: {
- v2_errorBoundary: true,
- v2_headers: true,
- v2_meta: true,
- v2_normalizeFormMethod: true,
- v2_routeConvention: true,
- v2_dev: {
- port: process.env.HMR_SERVER_PORT || 8002,
- },
- },
-};
diff --git a/sample-apps/payment-customizations/shopify.web.toml b/sample-apps/payment-customizations/shopify.web.toml
index 854d0dac..b0a0f29e 100644
--- a/sample-apps/payment-customizations/shopify.web.toml
+++ b/sample-apps/payment-customizations/shopify.web.toml
@@ -1,9 +1,7 @@
-name = "remix"
+name = "React Router"
roles = ["frontend", "backend"]
-webhooks_path = "/webhooks"
+webhooks_path = "/webhooks/app/uninstalled"
[commands]
-dev = "npm exec remix dev"
-
-[hmr_server]
-http_paths = ["/ping"]
+predev = "npx prisma generate"
+dev = "npx prisma migrate deploy && npm exec react-router dev"
diff --git a/sample-apps/payment-customizations/tsconfig.json b/sample-apps/payment-customizations/tsconfig.json
index 5097ec65..e053c519 100644
--- a/sample-apps/payment-customizations/tsconfig.json
+++ b/sample-apps/payment-customizations/tsconfig.json
@@ -1,25 +1,22 @@
{
- "include": ["**/*.js", "**/*.jsx"],
- "compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "ES2019"],
- "isolatedModules": true,
- "esModuleInterop": true,
- "jsx": "react-jsx",
- "moduleResolution": "node16",
- "resolveJsonModule": true,
- "target": "ES2019",
- "strict": true,
- "allowJs": true,
- "checkJs": true,
- "noImplicitAny": false,
- "forceConsistentCasingInFileNames": true,
- "baseUrl": ".",
- "paths": {
- "~/*": ["./app/*"]
- },
- "types": [
- "@shopify/app-bridge-types"
- ],
- "noEmit": true
- }
-}
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx", ".react-router/types/**/*"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "strict": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "removeComments": false,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "target": "ES2022",
+ "baseUrl": ".",
+ "types": ["@react-router/node", "vite/client"],
+ "rootDirs": [".", "./.react-router/types"]
+ }
+ }
\ No newline at end of file
diff --git a/sample-apps/payment-customizations/vite.config.js b/sample-apps/payment-customizations/vite.config.js
new file mode 100644
index 00000000..2c87fd7c
--- /dev/null
+++ b/sample-apps/payment-customizations/vite.config.js
@@ -0,0 +1,59 @@
+import { reactRouter } from "@react-router/dev/vite";
+import { defineConfig } from "vite";
+
+// 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 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 ||
+ 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: {
+ allowedHosts: [host],
+ cors: {
+ preflightContinue: true,
+ },
+ 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: [
+ reactRouter(),
+ ],
+ build: {
+ assetsInlineLimit: 0,
+ },
+ optimizeDeps: {
+ include: ["@shopify/app-bridge-react", "@shopify/polaris"],
+ },
+});
\ No newline at end of file