import { useState } from "react"; import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useLoaderData, useNavigation } from "@remix-run/react"; import { Page, Card, BlockStack, InlineStack, Text, Button, Select, TextField, IndexTable, EmptyState, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import type { Method } from "@prisma/client"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { formatPriceLabel } from "../lib/currency"; import { getShopTier } from "../services/billing.server"; import { tierAtLeast } from "../lib/billing-plans"; import { UpsellState } from "../components/UpsellState"; const METHODS: Method[] = ["SHIPPING", "LOCAL_DELIVERY", "PICKUP"]; const KEYED_BY_OPTIONS = [ { label: "Zone", value: "zone" }, { label: "Distance band", value: "distance" }, ]; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const tier = await getShopTier(session.shop); if (!tierAtLeast(tier, "growth")) { return { gated: true as const, tier }; } const [rates, zones] = await Promise.all([ db.rate.findMany({ where: { shopDomain: session.shop }, include: { zone: true }, orderBy: { createdAt: "asc" } }), db.zone.findMany({ where: { shopDomain: session.shop }, include: { location: true }, orderBy: { name: "asc" } }), ]); return { gated: false as const, rates, zones }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.admin(request); const tier = await getShopTier(session.shop); if (!tierAtLeast(tier, "growth")) { return data({ errors: { name: "Delivery rates need the Growth plan or higher." } }, { status: 403 }); } const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "delete") { const id = String(formData.get("id") || ""); await db.rate.deleteMany({ where: { id, shopDomain: session.shop } }); return data({ ok: true }); } const method = formData.get("method") as Method; const name = String(formData.get("name") || "").trim(); const keyedBy = String(formData.get("keyedBy") || "zone"); const zoneId = String(formData.get("zoneId") || "") || null; const priceRaw = String(formData.get("price") || ""); const minDistanceRaw = String(formData.get("minDistanceKm") || ""); const maxDistanceRaw = String(formData.get("maxDistanceKm") || ""); const errors: Record = {}; if (!name) errors.name = "Name is required"; if (!priceRaw || Number.isNaN(Number(priceRaw))) errors.price = "Price is required"; if (keyedBy === "zone" && !zoneId) errors.zoneId = "Choose a zone"; if (Object.keys(errors).length > 0) { return data({ errors }); } await db.rate.create({ data: { shopDomain: session.shop, method, name, keyedBy, zoneId: keyedBy === "zone" ? zoneId : null, priceCents: Math.round(Number(priceRaw) * 100), minDistanceKm: keyedBy === "distance" && minDistanceRaw ? Number(minDistanceRaw) : null, maxDistanceKm: keyedBy === "distance" && maxDistanceRaw ? Number(maxDistanceRaw) : null, }, }); return data({ ok: true }); }; export default function RatesIndex() { const loaderData = useLoaderData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; if (loaderData.gated) { return ( ); } const { rates, zones } = loaderData; if (zones.length === 0) { return ( Zone-keyed rates need at least one zone to attach to. ); } return ( {rates.length === 0 ? (
No rates yet. Without a rate, checkout uses Shopify's own configured shipping rates.
) : ( {rates.map((rate, index) => ( {rate.name} {rate.method.replace("_", " ")} {rate.keyedBy} {rate.keyedBy === "zone" ? (rate.zone?.name ?? "—") : `${rate.minDistanceKm ?? 0}–${rate.maxDistanceKm ?? "∞"} km`} {formatPriceLabel(rate.priceCents)}
))}
)}
); } function AddRateForm({ zones, isSubmitting, }: { zones: Array<{ id: string; name: string; location: { name: string } }>; isSubmitting: boolean; }) { const [keyedBy, setKeyedBy] = useState("zone"); const [name, setName] = useState(""); const [price, setPrice] = useState(""); const [minDistanceKm, setMinDistanceKm] = useState(""); const [maxDistanceKm, setMaxDistanceKm] = useState(""); return (
Add a rate {keyedBy === "zone" ? (