Loading...
Loading...
Compare original and translation side by side
useFetcheruseFetcher┌─────────────────────────────────────────────────────────────┐
│ Shopify Admin │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Your App (iframe) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ Frontend │ ───── │ Remix Backend │ │ │
│ │ │ (React) │ │ (loaders/actions) │ │ │
│ │ └──────────────┘ └──────────────────────┘ │ │
│ │ │ │ │
│ └────────────────────────────────────│────────────────┘ │
│ │ │
└───────────────────────────────────────│──────────────────────┘
│
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Prisma │ │ Shopify │
│ (your DB) │ │ Admin API │
└─────────────┘ └─────────────┘┌─────────────────────────────────────────────────────────────┐
│ Shopify Admin │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Your App (iframe) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ Frontend │ ───── │ Remix Backend │ │ │
│ │ │ (React) │ │ (loaders/actions) │ │ │
│ │ └──────────────┘ └──────────────────────┘ │ │
│ │ │ │ │
│ └────────────────────────────────────│────────────────┘ │
│ │ │
└───────────────────────────────────────│──────────────────────┘
│
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Prisma │ │ Shopify │
│ (your DB) │ │ Admin API │
└─────────────┘ └─────────────┘// app/routes/app.dashboard.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
// Authenticate and get admin API access
const { session, admin } = await authenticate.admin(request);
// Fetch from Shopify
const shopResponse = await admin.graphql(`
query { shop { name myshopifyDomain } }
`);
const { data: shopData } = await shopResponse.json();
// Fetch from your database
const settings = await db.appSettings.findUnique({
where: { shop: session.shop }
});
return json({
shop: shopData.shop,
settings
});
};
export default function Dashboard() {
const { shop, settings } = useLoaderData<typeof loader>();
return (
<Page title={`Dashboard - ${shop.name}`}>
{/* Your UI */}
</Page>
);
}// app/routes/app.dashboard.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
// Authenticate and get admin API access
const { session, admin } = await authenticate.admin(request);
// Fetch from Shopify
const shopResponse = await admin.graphql(`
query { shop { name myshopifyDomain } }
`);
const { data: shopData } = await shopResponse.json();
// Fetch from your database
const settings = await db.appSettings.findUnique({
where: { shop: session.shop }
});
return json({
shop: shopData.shop,
settings
});
};
export default function Dashboard() {
const { shop, settings } = useLoaderData<typeof loader>();
return (
<Page title={`Dashboard - ${shop.name}`}>
{/* Your UI */}
</Page>
);
}// app/routes/app.campaigns.$id.tsx
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
const { id } = params;
const campaign = await db.campaign.findFirst({
where: {
id,
shop: session.shop
}
});
if (!campaign) {
throw new Response("Not found", { status: 404 });
}
return json({ campaign });
};// app/routes/app.campaigns.$id.tsx
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
const { id } = params;
const campaign = await db.campaign.findFirst({
where: {
id,
shop: session.shop
}
});
if (!campaign) {
throw new Response("Not found", { status: 404 });
}
return json({ campaign });
};// app/routes/app.settings.tsx
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { session } = await authenticate.admin(request);
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "updateSettings") {
const enabled = formData.get("enabled") === "true";
const message = formData.get("message") as string;
await db.appSettings.upsert({
where: { shop: session.shop },
create: { shop: session.shop, enabled, message },
update: { enabled, message }
});
return json({ success: true });
}
return json({ error: "Unknown action" }, { status: 400 });
};
export default function Settings() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input type="hidden" name="intent" value="updateSettings" />
{/* Form fields */}
<Button submit loading={isSubmitting}>
Save
</Button>
</Form>
);
}// app/routes/app.settings.tsx
import { json, type ActionFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { session } = await authenticate.admin(request);
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "updateSettings") {
const enabled = formData.get("enabled") === "true";
const message = formData.get("message") as string;
await db.appSettings.upsert({
where: { shop: session.shop },
create: { shop: session.shop, enabled, message },
update: { enabled, message }
});
return json({ success: true });
}
return json({ error: "Unknown action" }, { status: 400 });
};
export default function Settings() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<Form method="post">
<input type="hidden" name="intent" value="updateSettings" />
{/* Form fields */}
<Button submit loading={isSubmitting}>
Save
</Button>
</Form>
);
}import { useFetcher } from "@remix-run/react";
function CampaignRow({ campaign }) {
const fetcher = useFetcher();
const isUpdating = fetcher.state !== "idle";
const toggleStatus = () => {
fetcher.submit(
{
intent: "toggleStatus",
campaignId: campaign.id,
enabled: String(!campaign.enabled)
},
{ method: "post", action: "/app/campaigns" }
);
};
return (
<ResourceItem id={campaign.id}>
<Text>{campaign.name}</Text>
<Button
onClick={toggleStatus}
loading={isUpdating}
>
{campaign.enabled ? "Disable" : "Enable"}
</Button>
</ResourceItem>
);
}import { useFetcher } from "@remix-run/react";
function CampaignRow({ campaign }) {
const fetcher = useFetcher();
const isUpdating = fetcher.state !== "idle";
const toggleStatus = () => {
fetcher.submit(
{
intent: "toggleStatus",
campaignId: campaign.id,
enabled: String(!campaign.enabled)
},
{ method: "post", action: "/app/campaigns" }
);
};
return (
<ResourceItem id={campaign.id}>
<Text>{campaign.name}</Text>
<Button
onClick={toggleStatus}
loading={isUpdating}
>
{campaign.enabled ? "Disable" : "Enable"}
</Button>
</ResourceItem>
);
}// app/routes/api.widget-config.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
// For app proxy requests or authenticated API calls
const { session } = await authenticate.admin(request);
const config = await db.widgetConfig.findUnique({
where: { shop: session.shop }
});
return json(config);
};
export const action = async ({ request }: ActionFunctionArgs) => {
const { session } = await authenticate.admin(request);
const body = await request.json();
// Whitelist allowed fields - never pass raw body to database
const { theme, position, welcomeMessage } = body;
const updated = await db.widgetConfig.update({
where: { shop: session.shop },
data: { theme, position, welcomeMessage }
});
return json(updated);
};// app/routes/api.widget-config.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
// For app proxy requests or authenticated API calls
const { session } = await authenticate.admin(request);
const config = await db.widgetConfig.findUnique({
where: { shop: session.shop }
});
return json(config);
};
export const action = async ({ request }: ActionFunctionArgs) => {
const { session } = await authenticate.admin(request);
const body = await request.json();
// Whitelist allowed fields - never pass raw body to database
const { theme, position, welcomeMessage } = body;
const updated = await db.widgetConfig.update({
where: { shop: session.shop },
data: { theme, position, welcomeMessage }
});
return json(updated);
};// app/routes/webhooks.tsx
import { type ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, payload } = await authenticate.webhook(request);
switch (topic) {
case "ORDERS_CREATE":
await handleOrderCreated(shop, payload);
break;
case "APP_UNINSTALLED":
await handleAppUninstalled(shop);
break;
}
return new Response();
};// app/routes/webhooks.tsx
import { type ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, payload } = await authenticate.webhook(request);
switch (topic) {
case "ORDERS_CREATE":
await handleOrderCreated(shop, payload);
break;
case "APP_UNINSTALLED":
await handleAppUninstalled(shop);
break;
}
return new Response();
};// Session is available after authenticate.admin()
const { session, admin } = await authenticate.admin(request);
// session contains:
// - session.shop: "store.myshopify.com"
// - session.accessToken: OAuth token
// - session.scope: granted scopes// Session is available after authenticate.admin()
const { session, admin } = await authenticate.admin(request);
// session contains:
// - session.shop: "store.myshopify.com"
// - session.accessToken: OAuth token
// - session.scope: granted scopesexport const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
const hasOrdersScope = session.scope?.includes("read_orders");
if (!hasOrdersScope) {
// Redirect to re-auth or show error
throw new Response("Missing required scope", { status: 403 });
}
// Continue...
};export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request);
const hasOrdersScope = session.scope?.includes("read_orders");
if (!hasOrdersScope) {
// Redirect to re-auth or show error
throw new Response("Missing required scope", { status: 403 });
}
// Continue...
};app/
├── routes/
│ ├── app._index.tsx # /app (dashboard)
│ ├── app.settings.tsx # /app/settings
│ ├── app.campaigns._index.tsx # /app/campaigns (list)
│ ├── app.campaigns.$id.tsx # /app/campaigns/:id (detail)
│ ├── app.campaigns.new.tsx # /app/campaigns/new (create)
│ ├── api.widget-config.tsx # /api/widget-config
│ └── webhooks.tsx # /webhooks
├── components/
│ └── ...
├── shopify.server.ts # Shopify app config
└── db.server.ts # Prisma clientapp/
├── routes/
│ ├── app._index.tsx # /app (dashboard)
│ ├── app.settings.tsx # /app/settings
│ ├── app.campaigns._index.tsx # /app/campaigns (list)
│ ├── app.campaigns.$id.tsx # /app/campaigns/:id (detail)
│ ├── app.campaigns.new.tsx # /app/campaigns/new (create)
│ ├── api.widget-config.tsx # /api/widget-config
│ └── webhooks.tsx # /webhooks
├── components/
│ └── ...
├── shopify.server.ts # Shopify app config
└── db.server.ts # Prisma clientauthenticate.admin(request)session.shopnavigation.statefetcher.stateuseLoaderData<typeof loader>()authenticate.admin(request)session.shopnavigation.statefetcher.stateuseLoaderData<typeof loader>()