metatroncubeswdev d150509978
Some checks failed
CI / Lint, Unit & Integration Tests (push) Has been cancelled
feat(phase-8): Billing API with server-side feature gating
Add real Shopify Billing API integration: Free/Starter/Growth/Pro plans
(app/lib/billing-plans.ts, priced per PRODUCT_STRATEGY.md §6) wired into
shopify.server.ts's billing config, a merchant-facing plan page
(app/routes/app.billing.tsx) using billing.request/billing.cancel, and
webhooks.app_subscriptions.update.tsx as the durable sync path for
Shop.tier (fires even when a merchant cancels from Shopify's own billing
page, not just from this app).

Gate the features actually built so far in both loader and action (never
just hidden in the UI, so a direct POST can't bypass a tier limit):
delivery zones/rates require Growth+, the dispatch dashboard requires
Starter+, and location count is capped per tier (Free=1, Starter=3,
Growth/Pro=unlimited). Split pure tier logic (app/lib/billing-plans.ts)
from DB-backed reads/writes (app/services/billing.server.ts) so the
client-rendered UpsellState component can import the Tier type without
pulling server code into the client bundle — same split as currency.ts.

Covered by tests/unit/billing-plans.test.ts (pure tier ranking/mapping)
and tests/integration/billing.test.ts (tier persistence and location-limit
enforcement against live Postgres).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:29:42 -04:00

48 lines
1.6 KiB
TypeScript

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<typeof loader>();
return (
<AppProvider isEmbeddedApp apiKey={apiKey}>
<NavMenu>
<Link to="/app" rel="home">
Home
</Link>
<Link to="/app/locations">Locations</Link>
<Link to="/app/slots">Weekly slots</Link>
<Link to="/app/blackouts">Blackout dates</Link>
<Link to="/app/zones">Delivery zones</Link>
<Link to="/app/rates">Delivery rates</Link>
<Link to="/app/dashboard">Dispatch dashboard</Link>
<Link to="/app/billing">Billing</Link>
</NavMenu>
<Outlet />
</AppProvider>
);
}
// 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);
};