From 2faa38c011a153c1b67ef3bcfb2a87f9b8fa8564 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Mon, 8 Jun 2026 21:57:07 +0100 Subject: [PATCH] PAYG FE: rework promo as PaygFreeLeader/Member; reframe billable scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the side-by-side card promo (EditorPlanPromo) in favour of two views that mirror the subscribed PaygLeader/PaygMember dashboards visually. The free view now feels like an extension of the paid view rather than a different page, which makes turning on Processor feel like enabling a switch rather than buying a product. New billable framing (product call set 2026-06): Free, always — anything clicked directly in the Stirling UI: viewing, editing, merging, splitting, signing, watermarking, compression, conversion, OCR via the interactive UI. Counts toward 500/month free tier — and metered above that: automation pipelines, AI tools, API calls. Note: backend design doc + pricing schema don't reflect this yet — FE is the source of truth in the meantime; backend will catch up. Components: PaygFreeLeader - Mirrors PaygLeader: subtitle + role pill header, hero usage card showing 62 / 500 free operations with progress bar + "Plenty left" status, identical visual language to the subscribed view - Below the hero: a gradient-bordered CTA card ("Turn on the Processor plan") with three benefit bullets (Automation / AI / API access) and footer row containing the CTA button + reassurance copy - Bottom: side-by-side explainer cards — "Always free" vs "Counts toward 500/month" — using the same colour tokens as the hero - CTA opens the existing UpgradeModal PaygFreeMember - Same header + hero pattern but with team-member pill - Replaces the CTA with a lock-icon note explaining the owner can enable Processor - Same bottom explainer cards but worded for the member viewpoint ("Always free for you" / "Shared with the team") UpgradeModal copy rewrite: - Promise card now reads "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." - "Set your monthly spend ceiling" section text updated to "Your first 500 automation / AI / API operations every month are free" - "What we count toward billing" card now a bulleted list of the three billable categories plus an italic line explicitly listing UI tools that stay free Dev preview slots reworked: - Was: free / upgrade-modal / active-leader / active-member - Now: free-leader / free-member / subscribed-leader / subscribed-member - "Upgrade modal" slot removed — the modal now opens from inside free-leader, which is the only real entry point Deleted: EditorPlanPromo.tsx + .css (superseded by PaygFreeLeader). Screenshots: payg-flow-009-free-leader-v1.png (first cut — stacked header) payg-flow-010-free-leader-v2.png (Mantine Group header, polished CTA) payg-flow-011-free-member.png (member view, lock note instead of CTA) payg-flow-012-modal-new-copy.png (modal with billable framing reworked) --- .../config/configSections/EditorPlanPromo.css | 249 ------------- .../config/configSections/EditorPlanPromo.tsx | 174 --------- .../shared/config/configSections/PaygFree.css | 201 +++++++++++ .../shared/config/configSections/PaygFree.tsx | 333 ++++++++++++++++++ .../config/configSections/UpgradeModal.tsx | 38 +- .../editor/src/saas/routes/DevPaygPreview.tsx | 87 ++--- 6 files changed, 589 insertions(+), 493 deletions(-) delete mode 100644 frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.css delete mode 100644 frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.tsx create mode 100644 frontend/editor/src/saas/components/shared/config/configSections/PaygFree.css create mode 100644 frontend/editor/src/saas/components/shared/config/configSections/PaygFree.tsx diff --git a/frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.css b/frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.css deleted file mode 100644 index e642f46d72..0000000000 --- a/frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.css +++ /dev/null @@ -1,249 +0,0 @@ -/* Editor → Processor plan promo. Shown to free users in place of the PAYG - dashboard. Uses the same theme tokens as Payg.css. */ - -.epp { - --epp-accent: #0a8bff; - --epp-accent-2: #6c5ce7; - --epp-accent-soft: rgba(10, 139, 255, 0.08); - --epp-success: #10b981; - --epp-radius: 18px; - --epp-card-bg: var(--bg-surface); - --epp-card-border: var(--border-default); - --epp-divider: var(--border-subtle); - display: flex; - flex-direction: column; - gap: 28px; - padding: 4px 4px 24px; -} - -[data-mantine-color-scheme="dark"] .epp { - --epp-card-bg: #313842; - --epp-card-border: #3d444e; - --epp-accent-soft: rgba(10, 139, 255, 0.12); - --epp-divider: #3d444e; -} - -/* ── Hero ─────────────────────────────────────────────────────────────── */ -.epp-hero { - text-align: center; - padding: 12px 0 0; -} -.epp-hero__eyebrow { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 12px; - margin-bottom: 14px; - background: var(--epp-accent-soft); - color: var(--epp-accent); - border-radius: 999px; - font-size: 0.7rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; -} -.epp-hero__title { - margin: 0; - font-size: 2.5rem; - font-weight: 700; - letter-spacing: -0.03em; - line-height: 1.1; - color: var(--text-primary); -} -.epp-hero__title .epp-hero__accent { - background: linear-gradient(135deg, var(--epp-accent) 0%, var(--epp-accent-2) 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; -} -.epp-hero__subtitle { - margin: 14px auto 0; - max-width: 540px; - font-size: 1rem; - line-height: 1.55; - color: var(--text-muted); -} - -/* ── Cards row ────────────────────────────────────────────────────────── */ -.epp-cards { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - align-items: stretch; -} -@media (max-width: 720px) { - .epp-cards { - grid-template-columns: 1fr; - } -} - -.epp-card { - background: var(--epp-card-bg); - border: 1px solid var(--epp-card-border); - border-radius: var(--epp-radius); - padding: 26px 26px 22px; - display: flex; - flex-direction: column; - gap: 14px; - position: relative; - transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; -} -.epp-card[data-emphasis="true"] { - border-color: transparent; - background: - linear-gradient(var(--epp-card-bg), var(--epp-card-bg)) padding-box, - linear-gradient(135deg, var(--epp-accent) 0%, var(--epp-accent-2) 100%) border-box; - border: 1.5px solid transparent; - box-shadow: 0 10px 28px -16px rgba(10, 139, 255, 0.35); -} -.epp-card[data-emphasis="true"]:hover { - transform: translateY(-2px); - box-shadow: 0 18px 40px -16px rgba(10, 139, 255, 0.45); -} - -/* Card header — name + inline status pill on the same row, no orphan badges */ -.epp-card__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin-bottom: 2px; -} -.epp-card__name { - font-size: 1.05rem; - font-weight: 700; - letter-spacing: -0.005em; - color: var(--text-primary); -} -.epp-card__pill { - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - padding: 3px 9px; - border-radius: 999px; -} -.epp-card__pill[data-tone="current"] { - background: var(--epp-divider); - color: var(--text-muted); -} -.epp-card__pill[data-tone="upgrade"] { - background: linear-gradient(135deg, var(--epp-accent) 0%, var(--epp-accent-2) 100%); - color: white; -} - -.epp-card__price { - display: flex; - align-items: baseline; - gap: 6px; - margin: 4px 0 -2px; -} -.epp-card__price-amount { - font-size: 2.1rem; - font-weight: 700; - letter-spacing: -0.025em; - color: var(--text-primary); - line-height: 1; -} -.epp-card__price-period { - font-size: 0.875rem; - color: var(--text-muted); -} -.epp-card__price-sub { - font-size: 0.78rem; - color: var(--text-muted); - margin-top: 4px; - font-variant-numeric: tabular-nums; -} - -.epp-card__tagline { - font-size: 0.875rem; - color: var(--text-muted); - line-height: 1.45; -} - -.epp-card__features { - list-style: none; - margin: 0; - padding: 12px 0 4px; - display: flex; - flex-direction: column; - gap: 9px; - flex-grow: 1; - border-top: 1px solid var(--epp-divider); -} -.epp-card__feature { - display: flex; - align-items: flex-start; - gap: 10px; - font-size: 0.875rem; - color: var(--text-primary); - line-height: 1.4; -} -.epp-card__feature-icon { - flex-shrink: 0; - margin-top: 1px; - color: var(--epp-success); - font-size: 1rem; -} -.epp-card[data-emphasis="true"] .epp-card__feature-icon { - color: var(--epp-accent); -} - -.epp-card__cta { - display: block; - width: 100%; - padding: 13px 18px; - border-radius: 11px; - border: none; - font-weight: 600; - font-size: 0.95rem; - cursor: pointer; - transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease; - margin-top: 10px; -} -.epp-card__cta[data-variant="primary"] { - background: linear-gradient(135deg, var(--epp-accent) 0%, var(--epp-accent-2) 100%); - color: white; -} -.epp-card__cta[data-variant="primary"]:hover { - transform: translateY(-1px); - box-shadow: 0 8px 22px -6px rgba(10, 139, 255, 0.55); -} -.epp-card__cta[data-variant="neutral"] { - background: transparent; - color: var(--text-muted); - border: 1px dashed var(--epp-divider); - cursor: default; -} - -.epp-card__footnote { - font-size: 0.75rem; - color: var(--text-muted); - text-align: center; - margin-top: 4px; -} - -/* ── Reassurance strip ────────────────────────────────────────────────── */ -.epp-footer { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 18px 20px; - background: var(--epp-accent-soft); - border-radius: 12px; - font-size: 0.875rem; - color: var(--text-primary); - line-height: 1.5; - text-align: center; - flex-wrap: wrap; -} -.epp-footer__highlight { - font-weight: 700; -} -.epp-footer__divider { - color: var(--text-muted); - opacity: 0.5; - margin: 0 4px; -} diff --git a/frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.tsx b/frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.tsx deleted file mode 100644 index 3650825791..0000000000 --- a/frontend/editor/src/saas/components/shared/config/configSections/EditorPlanPromo.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Editor → Processor plan promo page. - * - * Shown to free (unsubscribed) users in the Plan / Billing nav slot, in place of - * the {@code Payg.tsx} dashboard that subscribed users see. The single job of this - * screen is to make the "editing is always free; you only pay when our servers - * process documents" promise unmissable, and to make the upgrade flow one click - * away. - * - * Marketing names (per 2026-06 product call): - * - "Editor plan" = free; everything that runs in the browser - * - "Processor plan" = pay-as-you-go; server-side jobs, AI, automation - * - * Code keeps the internal name PAYG. Renaming the FE strings later is a one-place - * change via the i18n keys this component reads. - */ -import React from "react"; -import CheckIcon from "@mui/icons-material/CheckRounded"; -import BoltIcon from "@mui/icons-material/BoltRounded"; -import ShieldIcon from "@mui/icons-material/ShieldOutlined"; -// eslint-disable-next-line no-restricted-imports -import "./EditorPlanPromo.css"; - -interface EditorPlanPromoProps { - /** - * Called when the user clicks the "Upgrade to Processor" CTA. Parent owns the - * modal so the promo stays presentation-only. - */ - onUpgradeClick: () => void; -} - -const EDITOR_FEATURES = [ - "View any PDF — fast in-browser viewer", - "Edit pages: reorder, split, merge", - "Annotate, highlight, redact", - "Fill and sign forms", - "Add images, watermarks, page numbers", - "Export PDFs and password-protect", -]; - -const PROCESSOR_FEATURES = [ - "Everything in the Editor plan", - "OCR — extract text from scans", - "Compress + convert (Word, Excel, images, …)", - "AI tools — summarise, redact, classify, AI-OCR", - "Server-side automation pipelines", - "Batch processing on the server", -]; - -export default function EditorPlanPromo({ - onUpgradeClick, -}: EditorPlanPromoProps) { - return ( -
- {/* Hero — anchors the promise. Eyebrow pill + headline + supporting line. */} -
-
- - No card needed to edit -
-

- Editing is{" "} - free, forever. -

-

- You only pay when our servers process documents — OCR, conversions, - AI, automation. Everything else stays free. -

-
- - {/* Two-card comparison. Left is the current plan (neutral); right is the - upgrade option (gradient border + interactive). */} -
-
-
- Editor plan - - Current - -
-
- Free - forever -
-

- Everything that runs in your browser. No account upgrade required. -

-
    - {EDITOR_FEATURES.map((feature) => ( -
  • - - {feature} -
  • - ))} -
- -

No payment method required.

-
- -
-
- - - Processor plan - - - Upgrade - -
-
- Pay-as-you-go -
-

- From $0.02 per processed document · monthly cap is yours to set -

-

- Unlock server-side tools. You set a monthly ceiling; you only pay - for what's actually processed. -

-
    - {PROCESSOR_FEATURES.map((feature) => ( -
  • - - {feature} -
  • - ))} -
- -

- Set your monthly ceiling · Cancel anytime -

-
-
- - {/* Bottom reinforcement strip. Belt to the hero's braces. */} -
- - The Editor stays free regardless. - - · - - Processor billing only kicks in when you upload a document for our - servers to process — and only above your free-tier allowance. - -
-
- ); -} diff --git a/frontend/editor/src/saas/components/shared/config/configSections/PaygFree.css b/frontend/editor/src/saas/components/shared/config/configSections/PaygFree.css new file mode 100644 index 0000000000..46da603c11 --- /dev/null +++ b/frontend/editor/src/saas/components/shared/config/configSections/PaygFree.css @@ -0,0 +1,201 @@ +/* Styles for the free-tier Plan views (PaygFreeLeader + PaygFreeMember). + Extends Payg.css — the hero + bar + status pill reuse those classes so the + visual reads as a continuation of the Processor dashboard. New classes here + are prefixed `paygf-`. */ + +.payg-hero__head-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + flex-wrap: nowrap; +} + +/* ── Leader CTA card ──────────────────────────────────────────────────── */ + +.paygf-cta { + margin-top: var(--payg-gap); + padding: 22px 24px; + border-radius: var(--payg-radius); + background: var(--payg-card-bg); + /* Gradient border using padding-box / border-box trick so the inner bg + stays solid + the outline gets the brand gradient. */ + border: 1.5px solid transparent; + background: + linear-gradient(var(--payg-card-bg), var(--payg-card-bg)) padding-box, + linear-gradient(135deg, var(--payg-accent) 0%, #6c5ce7 100%) border-box; + box-shadow: 0 10px 28px -18px rgba(10, 139, 255, 0.4); + display: flex; + flex-direction: column; + gap: 16px; +} + +.paygf-cta__heading-row { + display: flex; + gap: 14px; + align-items: center; +} +.paygf-cta__icon { + flex-shrink: 0; + padding: 10px; + border-radius: 12px; + background: linear-gradient(135deg, var(--payg-accent) 0%, #6c5ce7 100%); + color: white !important; + font-size: 1.7rem !important; +} +.paygf-cta__heading-text { + flex: 1 1 auto; +} +.paygf-cta__title { + margin: 0; + font-size: 1.2rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.01em; + line-height: 1.25; +} +.paygf-cta__subtitle { + margin: 4px 0 0; + font-size: 0.9rem; + color: var(--text-muted); + line-height: 1.5; +} + +.paygf-cta__benefits { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px 16px; +} +.paygf-cta__benefits li { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 0.85rem; + color: var(--text-primary); + line-height: 1.45; +} +.paygf-cta__check { + flex-shrink: 0; + color: var(--payg-accent); + margin-top: 2px; +} + +/* Footer row: button on the left, reassurance text on the right. + Right-side text wraps to second line below button on narrow viewports. */ +.paygf-cta__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + flex-wrap: wrap; + padding-top: 4px; + border-top: 1px solid var(--payg-divider); + padding-top: 16px; +} +.paygf-cta__button { + padding: 13px 22px; + border: none; + border-radius: 11px; + background: linear-gradient(135deg, var(--payg-accent) 0%, #6c5ce7 100%); + color: white; + font-weight: 600; + font-size: 0.95rem; + font-family: inherit; + cursor: pointer; + transition: transform 120ms ease, box-shadow 120ms ease; + white-space: nowrap; +} +.paygf-cta__button:hover { + transform: translateY(-1px); + box-shadow: 0 8px 22px -6px rgba(10, 139, 255, 0.55); +} +.paygf-cta__reassurance { + margin: 0; + font-size: 0.78rem; + color: var(--text-muted); + text-align: right; +} + +/* ── Member ask-the-owner note ───────────────────────────────────────── */ + +.paygf-member-note { + margin-top: var(--payg-gap); + padding: 22px 24px; + border-radius: var(--payg-radius); + background: var(--payg-inset-bg); + border: 1px solid var(--payg-card-border); + display: flex; + gap: 14px; + align-items: flex-start; +} +.paygf-member-note__icon { + flex-shrink: 0; + color: var(--text-muted); + padding: 8px; + background: var(--payg-card-bg); + border-radius: 10px; + font-size: 1.6rem !important; +} +.paygf-member-note__title { + margin: 0; + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.005em; + line-height: 1.3; +} +.paygf-member-note__body { + margin: 6px 0 0; + font-size: 0.875rem; + color: var(--text-muted); + line-height: 1.5; +} + +/* ── Explainer (free vs metered) — shared by leader + member ──────────── */ + +.paygf-explainer { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-top: var(--payg-gap); +} +@media (max-width: 640px) { + .paygf-explainer { + grid-template-columns: 1fr; + } +} +.paygf-explainer__col { + padding: 18px 20px; + border-radius: 12px; + background: var(--payg-card-bg); + border: 1px solid var(--payg-card-border); +} +.paygf-explainer__label { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 10px; +} +.paygf-explainer__icon { + font-size: 1rem !important; +} +.paygf-explainer__icon--free { + color: #10b981; +} +.paygf-explainer__icon--paid { + color: var(--payg-accent); +} +.paygf-explainer__text { + margin: 0; + font-size: 0.85rem; + color: var(--text-primary); + line-height: 1.55; +} diff --git a/frontend/editor/src/saas/components/shared/config/configSections/PaygFree.tsx b/frontend/editor/src/saas/components/shared/config/configSections/PaygFree.tsx new file mode 100644 index 0000000000..77a63dab73 --- /dev/null +++ b/frontend/editor/src/saas/components/shared/config/configSections/PaygFree.tsx @@ -0,0 +1,333 @@ +/** + * Free-tier Plan views (leader + member) shown before the team subscribes to + * Processor. Mirrors the visual language of {@code Payg.tsx} so the upgrade + * feels like enabling a switch, not visiting a new product. + * + *

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 ( +

+
+
+
+
This billing period
+
+ + {snap.billableUsed.toLocaleString()} + + + / {snap.billableLimit.toLocaleString()} free operations + +
+
+
+ + {stateLabel} +
+
+ +
+
+
+ +
+ 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)}. +
+ + {pill} + +
+ ); +} + +// ─── Free LEADER: hero + upgrade CTA + what's free explainer ───────────── + +export function PaygFreeLeader() { + const snap = useFreeMock(); + const [upgradeOpen, setUpgradeOpen] = useState(false); + + return ( +
+ + + + + +
+
+ +
+

Turn on the Processor plan

+

+ Unlock more than 500 automation, AI, and API operations per + month. Set a monthly ceiling — you stay in control. +

+
+
+ +
    +
  • + + + Automation pipelines — chain tools, schedule + runs, batch process + +
  • +
  • + + + AI tools — summarise, classify, redact, AI-OCR + +
  • +
  • + + + 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. +

+
+
+
+ + setUpgradeOpen(false)} + onComplete={({ capUsd }) => { + setUpgradeOpen(false); + // eslint-disable-next-line no-alert + alert( + `Demo: subscription complete. Cap = ${ + capUsd === null ? "no cap" : `$${capUsd}/mo` + }. Real flow refreshes the wallet snapshot here.`, + ); + }} + /> +
+ ); +} + +// ─── Free MEMBER: hero + ask-owner note (no CTA) ───────────────────────── + +export function PaygFreeMember() { + const snap = useFreeMock(); + + return ( +
+ + + + + +
+ +
+

+ 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. +

+
+
+
+
+ ); +} diff --git a/frontend/editor/src/saas/components/shared/config/configSections/UpgradeModal.tsx b/frontend/editor/src/saas/components/shared/config/configSections/UpgradeModal.tsx index 5d3c6135ea..fb7b104f1f 100644 --- a/frontend/editor/src/saas/components/shared/config/configSections/UpgradeModal.tsx +++ b/frontend/editor/src/saas/components/shared/config/configSections/UpgradeModal.tsx @@ -205,16 +205,19 @@ function CapStep({ capUsd, setCapUsd, noCap, setNoCap, currency }: CapStepProps)
- 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.

@@ -275,12 +278,25 @@ function CapStep({ capUsd, setCapUsd, noCap, setNoCap, currency }: CapStepProps)
- - 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 (
setSlot(s)} style={{ - padding: "6px 12px", + padding: "6px 14px", borderRadius: 6, border: "1px solid #d8dce1", background: slot === s ? "#2563eb" : "white", @@ -72,51 +80,12 @@ export default function DevPaygPreview() { Not shipped to production. Removed before PR merge. -
- {slot === "free" && ( - setUpgradeOpen(true)} /> - )} - {slot === "upgrade-modal" && ( -
-

- The modal opens over whatever is behind it. Click below to - re-open if you've closed it. -

- -
- )} - {slot === "active-leader" && } - {slot === "active-member" && } +
+ {slot === "free-leader" && } + {slot === "free-member" && } + {slot === "subscribed-leader" && } + {slot === "subscribed-member" && }
- setUpgradeOpen(false)} - onComplete={({ capUsd }) => { - setUpgradeOpen(false); - // eslint-disable-next-line no-alert - alert( - `Demo: subscription complete. Cap = ${ - capUsd === null ? "no cap" : `$${capUsd}/mo` - }. Real flow would refresh the wallet snapshot here.`, - ); - }} - />
); }