Free tier model (set 2026-06): users only pay for
+ * automation, AI, and API operations. Anything done
+ * directly via the Stirling UI — viewing, editing, signing, merging, splitting,
+ * conversion, OCR, compression when used as an interactive click — is
+ * unmetered. The 500/month free allowance applies only to those three
+ * billable categories. Backend design doc + pricing schema will catch up to
+ * this in a follow-up; this is the FE source of truth in the meantime.
+ *
+ *
Two variants:
+ * - {@link PaygFreeLeader} — visible to the team owner; includes the "Turn
+ * on Processor" CTA that opens the upgrade modal.
+ * - {@link PaygFreeMember} — read-only; surfaces the team's free-tier
+ * consumption and explains that the owner can enable Processor.
+ */
+import React, { useMemo, useState } from "react";
+import { Group, Stack } from "@mantine/core";
+import BoltIcon from "@mui/icons-material/BoltRounded";
+import CheckIcon from "@mui/icons-material/CheckRounded";
+import LockIcon from "@mui/icons-material/LockOutlined";
+// eslint-disable-next-line no-restricted-imports
+import "./Payg.css";
+// eslint-disable-next-line no-restricted-imports
+import "./PaygFree.css";
+import UpgradeModal from "./UpgradeModal";
+
+// ─── Shared free-tier snapshot (mock for now) ────────────────────────────
+
+interface FreeSnapshot {
+ /** ISO yyyy-mm-dd. */
+ billingPeriodStart: string;
+ billingPeriodEnd: string;
+ /** Automation + AI + API operations used this cycle. */
+ billableUsed: number;
+ /** Free-tier ceiling. Hard-coded 500 for V1; will come from pricing_policy. */
+ billableLimit: number;
+}
+
+function useFreeMock(): FreeSnapshot {
+ return useMemo(() => {
+ const now = new Date();
+ const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
+ const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
+ const isoDay = (d: Date) => d.toISOString().slice(0, 10);
+ return {
+ billingPeriodStart: isoDay(periodStart),
+ billingPeriodEnd: isoDay(periodEnd),
+ billableUsed: 62,
+ billableLimit: 500,
+ };
+ }, []);
+}
+
+function formatPeriod(start: string, end: string): string {
+ const s = new Date(start);
+ const e = new Date(end);
+ const fmt = (d: Date) =>
+ d.toLocaleDateString("en-GB", { day: "numeric", month: "short" });
+ return `${fmt(s)} – ${fmt(e)}`;
+}
+
+function daysUntil(iso: string): number {
+ return Math.max(
+ 0,
+ Math.ceil((new Date(iso).getTime() - Date.now()) / 86_400_000),
+ );
+}
+
+// ─── Hero usage panel (shared by leader + member) ─────────────────────────
+
+interface FreeHeroProps {
+ snap: FreeSnapshot;
+}
+
+function FreeHero({ snap }: FreeHeroProps) {
+ const pct = Math.min(100, (snap.billableUsed / snap.billableLimit) * 100);
+ const daysLeft = daysUntil(snap.billingPeriodEnd);
+ const state =
+ pct >= 100 ? "DEGRADED" : pct >= 80 ? "WARNED" : "FULL";
+ const stateLabel =
+ state === "DEGRADED"
+ ? "Limit reached"
+ : state === "WARNED"
+ ? "Approaching limit"
+ : "Plenty left";
+
+ return (
+
+ Automation · AI · API requests
+ •
+
+ {daysLeft === 1
+ ? "Resets tomorrow"
+ : `Resets in ${daysLeft} days`}
+
+
+
+
+ );
+}
+
+// ─── Section header (shared) ─────────────────────────────────────────────
+// Mirrors the Mantine Group + payg-header__subtitle + role pill pattern in
+// Payg.tsx so the free view reads as a continuation of the subscribed view.
+
+interface SectionHeaderProps {
+ snap: FreeSnapshot;
+ /** Pill text on the right. */
+ pill: string;
+ /** LEADER pill colour treatment. */
+ leader?: boolean;
+}
+
+function SectionHeader({ snap, pill, leader }: SectionHeaderProps) {
+ return (
+
+
+ Editor plan — everything in the Stirling UI is free. Pay only for
+ automation, AI & API. Billing period{" "}
+ {formatPeriod(snap.billingPeriodStart, snap.billingPeriodEnd)}.
+
+
+
+ API access — call any Stirling endpoint
+ programmatically
+
+
+
+
+
+
+
+ From $5/month · Cancel anytime · Set your own cap first
+
+
+
+
+
+
+
+
+ Always free
+
+
+ Anything you click in the Stirling UI: viewing, editing, merging,
+ splitting, signing, watermarks, compression, conversion, OCR via
+ the interactive UI — never counted, never capped.
+
+
+
+
+
+ Counts toward 500/month
+
+
+ Automation pipelines (running tools without UI clicks), AI tools
+ (summaries, classification, AI-OCR), and API calls (programmatic
+ access). Above 500 you'll need Processor.
+
+ Want more than 500 automation, AI, or API operations a month?
+
+
+ Your team owner can enable the Processor plan and set a monthly
+ ceiling. Until then, everything you can do in the Stirling UI is
+ free — automation, AI, and API access are limited to the team's
+ 500/month free allowance.
+
+
+
+
+
+
+
+
+ Always free for you
+
+
+ Anything you click in the Stirling UI: viewing, editing, merging,
+ splitting, signing, watermarks, compression, conversion, OCR via
+ the interactive UI — never counted.
+
+
+
+
+
+ Shared with the team
+
+
+ Automation pipelines, AI tools, and API calls share a single
+ 500/month allowance across the whole team. If it's full, ask
+ your owner to enable Processor.
+
- Editing stays free.{" "}
- You only pay when our servers process documents — OCR, conversions,
- AI tools, automation. Browsing and editing in your browser never
- count toward your spend.
+
+ Everything in the Stirling UI stays free.
+ {" "}
+ You only pay for what comes from automation pipelines, AI tools, and
+ API calls — the things that don't need a person clicking through the
+ UI. Use the editor as much as you want, no charge.
Set your monthly spend ceiling
- We'll never charge above this. Free-tier documents don't count.
+ We'll never charge above this. Your first 500 automation / AI / API
+ operations every month are free.
-
- Documents we count toward billing
-
- Server-side jobs: OCR, format conversion, compression, AI tools,
- automation pipelines, batch processing. Anything where Stirling's
- servers do real work on your file.
+ What we count toward billing
+
+
+ Automation pipelines — chained tools or scheduled
+ runs that don't need clicks
+
+
+ AI tools — summarise, classify, redact, AI-OCR
+
+
+ API calls — programmatic access to any Stirling
+ endpoint
+
+
+
+ Anything you click directly in the Stirling UI — viewing, editing,
+ merging, splitting, signing, watermarking, compressing, OCR via the
+ UI — is always free, even past 500.
+
>
);
diff --git a/frontend/editor/src/saas/routes/DevPaygPreview.tsx b/frontend/editor/src/saas/routes/DevPaygPreview.tsx
index 6b6ebbd9c2..9a6e673232 100644
--- a/frontend/editor/src/saas/routes/DevPaygPreview.tsx
+++ b/frontend/editor/src/saas/routes/DevPaygPreview.tsx
@@ -2,31 +2,39 @@
* DEV-ONLY preview route for the new PAYG plan + upgrade flow UI. NOT shipped
* to production — gated by {@code import.meta.env.DEV} in {@code App.tsx}.
*
- * Purpose: lets me iterate on the visual design of EditorPlanPromo +
- * UpgradeModal + the wired-up Payg dashboard without going through auth + the
- * config-modal nav. Each preview slot just renders one of the new components
- * with mock data and a state toggle.
+ * Purpose: lets me iterate on the visual design of PaygFreeLeader,
+ * PaygFreeMember, and the upgrade modal without going through auth + the
+ * config-modal nav. Each preview slot just renders one component with mock
+ * data; the slot toggle at the top is the only chrome.
*
* Delete this file (and its route registration in App.tsx) before the bundle
* PR is marked ready-for-review.
*/
import React, { useState } from "react";
-import EditorPlanPromo from "@app/components/shared/config/configSections/EditorPlanPromo";
-import UpgradeModal from "@app/components/shared/config/configSections/UpgradeModal";
-import { PaygLeader, PaygMember } from "@app/components/shared/config/configSections/Payg";
+import {
+ PaygLeader,
+ PaygMember,
+} from "@app/components/shared/config/configSections/Payg";
+import {
+ PaygFreeLeader,
+ PaygFreeMember,
+} from "@app/components/shared/config/configSections/PaygFree";
-type PreviewSlot = "free" | "upgrade-modal" | "active-leader" | "active-member";
+type PreviewSlot =
+ | "free-leader"
+ | "free-member"
+ | "subscribed-leader"
+ | "subscribed-member";
const SLOT_LABELS: Record = {
- free: "Free user — Editor plan promo",
- "upgrade-modal": "Upgrade modal (cap → Stripe)",
- "active-leader": "Subscribed — LEADER dashboard",
- "active-member": "Subscribed — MEMBER dashboard",
+ "free-leader": "Free · LEADER",
+ "free-member": "Free · MEMBER",
+ "subscribed-leader": "Subscribed · LEADER",
+ "subscribed-member": "Subscribed · MEMBER",
};
export default function DevPaygPreview() {
- const [slot, setSlot] = useState("free");
- const [upgradeOpen, setUpgradeOpen] = useState(false);
+ const [slot, setSlot] = useState("free-leader");
return (