PAYG FE: rework promo as PaygFreeLeader/Member; reframe billable scope
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)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="epp" data-testid="editor-plan-promo">
|
||||
{/* Hero — anchors the promise. Eyebrow pill + headline + supporting line. */}
|
||||
<header className="epp-hero">
|
||||
<div className="epp-hero__eyebrow">
|
||||
<ShieldIcon style={{ fontSize: "0.85rem" }} />
|
||||
No card needed to edit
|
||||
</div>
|
||||
<h2 className="epp-hero__title">
|
||||
Editing is{" "}
|
||||
<span className="epp-hero__accent">free, forever.</span>
|
||||
</h2>
|
||||
<p className="epp-hero__subtitle">
|
||||
You only pay when our servers process documents — OCR, conversions,
|
||||
AI, automation. Everything else stays free.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* Two-card comparison. Left is the current plan (neutral); right is the
|
||||
upgrade option (gradient border + interactive). */}
|
||||
<div className="epp-cards">
|
||||
<article className="epp-card" data-testid="editor-plan-card">
|
||||
<div className="epp-card__head">
|
||||
<span className="epp-card__name">Editor plan</span>
|
||||
<span className="epp-card__pill" data-tone="current">
|
||||
Current
|
||||
</span>
|
||||
</div>
|
||||
<div className="epp-card__price">
|
||||
<span className="epp-card__price-amount">Free</span>
|
||||
<span className="epp-card__price-period">forever</span>
|
||||
</div>
|
||||
<p className="epp-card__tagline">
|
||||
Everything that runs in your browser. No account upgrade required.
|
||||
</p>
|
||||
<ul className="epp-card__features">
|
||||
{EDITOR_FEATURES.map((feature) => (
|
||||
<li key={feature} className="epp-card__feature">
|
||||
<CheckIcon className="epp-card__feature-icon" fontSize="small" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
type="button"
|
||||
className="epp-card__cta"
|
||||
data-variant="neutral"
|
||||
disabled
|
||||
>
|
||||
You're on this plan
|
||||
</button>
|
||||
<p className="epp-card__footnote">No payment method required.</p>
|
||||
</article>
|
||||
|
||||
<article
|
||||
className="epp-card"
|
||||
data-emphasis="true"
|
||||
data-testid="processor-plan-card"
|
||||
>
|
||||
<div className="epp-card__head">
|
||||
<span className="epp-card__name">
|
||||
<BoltIcon
|
||||
style={{
|
||||
fontSize: "1.05rem",
|
||||
marginRight: 4,
|
||||
verticalAlign: "-3px",
|
||||
color: "var(--epp-accent)",
|
||||
}}
|
||||
/>
|
||||
Processor plan
|
||||
</span>
|
||||
<span className="epp-card__pill" data-tone="upgrade">
|
||||
Upgrade
|
||||
</span>
|
||||
</div>
|
||||
<div className="epp-card__price">
|
||||
<span className="epp-card__price-amount">Pay-as-you-go</span>
|
||||
</div>
|
||||
<p className="epp-card__price-sub">
|
||||
From $0.02 per processed document · monthly cap is yours to set
|
||||
</p>
|
||||
<p className="epp-card__tagline">
|
||||
Unlock server-side tools. You set a monthly ceiling; you only pay
|
||||
for what's actually processed.
|
||||
</p>
|
||||
<ul className="epp-card__features">
|
||||
{PROCESSOR_FEATURES.map((feature) => (
|
||||
<li key={feature} className="epp-card__feature">
|
||||
<CheckIcon className="epp-card__feature-icon" fontSize="small" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
type="button"
|
||||
className="epp-card__cta"
|
||||
data-variant="primary"
|
||||
onClick={onUpgradeClick}
|
||||
data-testid="upgrade-cta"
|
||||
>
|
||||
Upgrade to Processor →
|
||||
</button>
|
||||
<p className="epp-card__footnote">
|
||||
Set your monthly ceiling · Cancel anytime
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{/* Bottom reinforcement strip. Belt to the hero's braces. */}
|
||||
<footer className="epp-footer">
|
||||
<span className="epp-footer__highlight">
|
||||
The Editor stays free regardless.
|
||||
</span>
|
||||
<span className="epp-footer__divider">·</span>
|
||||
<span>
|
||||
Processor billing only kicks in when you upload a document for our
|
||||
servers to process — and only above your free-tier allowance.
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p><b>Free tier model (set 2026-06):</b> users only pay for
|
||||
* <em>automation</em>, <em>AI</em>, and <em>API</em> 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 <em>only</em> 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.
|
||||
*
|
||||
* <p>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 (
|
||||
<div className="payg-hero" data-state={state}>
|
||||
<div className="payg-hero__inner">
|
||||
<div className="payg-hero__head-row">
|
||||
<div>
|
||||
<div className="payg-hero__eyebrow">This billing period</div>
|
||||
<div className="payg-hero__figure">
|
||||
<span className="payg-hero__spend">
|
||||
{snap.billableUsed.toLocaleString()}
|
||||
</span>
|
||||
<span className="payg-hero__cap">
|
||||
/ {snap.billableLimit.toLocaleString()} free operations
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="payg-status" data-state={state}>
|
||||
<span className="payg-status__dot" />
|
||||
{stateLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payg-bar">
|
||||
<div
|
||||
className="payg-bar__fill"
|
||||
data-state={state}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="payg-hero__meta">
|
||||
<span>Automation · AI · API requests</span>
|
||||
<span className="payg-hero__meta-dot">•</span>
|
||||
<span>
|
||||
{daysLeft === 1
|
||||
? "Resets tomorrow"
|
||||
: `Resets in ${daysLeft} days`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<Group justify="space-between" align="center" wrap="nowrap">
|
||||
<div className="payg-header__subtitle">
|
||||
Editor plan — everything in the Stirling UI is free. Pay only for
|
||||
automation, AI & API. Billing period{" "}
|
||||
{formatPeriod(snap.billingPeriodStart, snap.billingPeriodEnd)}.
|
||||
</div>
|
||||
<span className="payg-role-pill" data-leader={leader ? "true" : "false"}>
|
||||
{pill}
|
||||
</span>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Free LEADER: hero + upgrade CTA + what's free explainer ─────────────
|
||||
|
||||
export function PaygFreeLeader() {
|
||||
const snap = useFreeMock();
|
||||
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="payg">
|
||||
<Stack gap="lg">
|
||||
<SectionHeader snap={snap} pill="Team owner" leader />
|
||||
|
||||
<FreeHero snap={snap} />
|
||||
|
||||
<div className="paygf-cta">
|
||||
<div className="paygf-cta__heading-row">
|
||||
<BoltIcon className="paygf-cta__icon" />
|
||||
<div className="paygf-cta__heading-text">
|
||||
<h3 className="paygf-cta__title">Turn on the Processor plan</h3>
|
||||
<p className="paygf-cta__subtitle">
|
||||
Unlock more than 500 automation, AI, and API operations per
|
||||
month. Set a monthly ceiling — you stay in control.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="paygf-cta__benefits">
|
||||
<li>
|
||||
<CheckIcon className="paygf-cta__check" fontSize="small" />
|
||||
<span>
|
||||
<strong>Automation pipelines</strong> — chain tools, schedule
|
||||
runs, batch process
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<CheckIcon className="paygf-cta__check" fontSize="small" />
|
||||
<span>
|
||||
<strong>AI tools</strong> — summarise, classify, redact, AI-OCR
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<CheckIcon className="paygf-cta__check" fontSize="small" />
|
||||
<span>
|
||||
<strong>API access</strong> — call any Stirling endpoint
|
||||
programmatically
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="paygf-cta__footer">
|
||||
<button
|
||||
type="button"
|
||||
className="paygf-cta__button"
|
||||
onClick={() => setUpgradeOpen(true)}
|
||||
data-testid="turn-on-processor"
|
||||
>
|
||||
Turn on Processor →
|
||||
</button>
|
||||
<span className="paygf-cta__reassurance">
|
||||
From $5/month · Cancel anytime · Set your own cap first
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="paygf-explainer">
|
||||
<div className="paygf-explainer__col">
|
||||
<div className="paygf-explainer__label">
|
||||
<CheckIcon
|
||||
className="paygf-explainer__icon paygf-explainer__icon--free"
|
||||
fontSize="small"
|
||||
/>
|
||||
Always free
|
||||
</div>
|
||||
<p className="paygf-explainer__text">
|
||||
Anything you click in the Stirling UI: viewing, editing, merging,
|
||||
splitting, signing, watermarks, compression, conversion, OCR via
|
||||
the interactive UI — never counted, never capped.
|
||||
</p>
|
||||
</div>
|
||||
<div className="paygf-explainer__col">
|
||||
<div className="paygf-explainer__label">
|
||||
<BoltIcon
|
||||
className="paygf-explainer__icon paygf-explainer__icon--paid"
|
||||
fontSize="small"
|
||||
/>
|
||||
Counts toward 500/month
|
||||
</div>
|
||||
<p className="paygf-explainer__text">
|
||||
Automation pipelines (running tools without UI clicks), AI tools
|
||||
(summaries, classification, AI-OCR), and API calls (programmatic
|
||||
access). Above 500 you'll need Processor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<UpgradeModal
|
||||
open={upgradeOpen}
|
||||
onClose={() => 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.`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Free MEMBER: hero + ask-owner note (no CTA) ─────────────────────────
|
||||
|
||||
export function PaygFreeMember() {
|
||||
const snap = useFreeMock();
|
||||
|
||||
return (
|
||||
<div className="payg">
|
||||
<Stack gap="lg">
|
||||
<SectionHeader snap={snap} pill="Team member" />
|
||||
|
||||
<FreeHero snap={snap} />
|
||||
|
||||
<div className="paygf-member-note">
|
||||
<LockIcon className="paygf-member-note__icon" />
|
||||
<div>
|
||||
<h3 className="paygf-member-note__title">
|
||||
Want more than 500 automation, AI, or API operations a month?
|
||||
</h3>
|
||||
<p className="paygf-member-note__body">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="paygf-explainer">
|
||||
<div className="paygf-explainer__col">
|
||||
<div className="paygf-explainer__label">
|
||||
<CheckIcon
|
||||
className="paygf-explainer__icon paygf-explainer__icon--free"
|
||||
fontSize="small"
|
||||
/>
|
||||
Always free for you
|
||||
</div>
|
||||
<p className="paygf-explainer__text">
|
||||
Anything you click in the Stirling UI: viewing, editing, merging,
|
||||
splitting, signing, watermarks, compression, conversion, OCR via
|
||||
the interactive UI — never counted.
|
||||
</p>
|
||||
</div>
|
||||
<div className="paygf-explainer__col">
|
||||
<div className="paygf-explainer__label">
|
||||
<BoltIcon
|
||||
className="paygf-explainer__icon paygf-explainer__icon--paid"
|
||||
fontSize="small"
|
||||
/>
|
||||
Shared with the team
|
||||
</div>
|
||||
<p className="paygf-explainer__text">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -205,16 +205,19 @@ function CapStep({ capUsd, setCapUsd, noCap, setNoCap, currency }: CapStepProps)
|
||||
<div className="upm-promise">
|
||||
<ShieldIcon className="upm-promise__icon" fontSize="small" />
|
||||
<div>
|
||||
<span className="upm-promise__highlight">Editing stays free.</span>{" "}
|
||||
You only pay when our servers process documents — OCR, conversions,
|
||||
AI tools, automation. Browsing and editing in your browser never
|
||||
count toward your spend.
|
||||
<span className="upm-promise__highlight">
|
||||
Everything in the Stirling UI stays free.
|
||||
</span>{" "}
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="upm-section-title">Set your monthly spend ceiling</h3>
|
||||
<p className="upm-section-help">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="upm-cap-presets" role="radiogroup" aria-label="Monthly cap preset">
|
||||
@@ -275,12 +278,25 @@ function CapStep({ capUsd, setCapUsd, noCap, setNoCap, currency }: CapStepProps)
|
||||
</label>
|
||||
|
||||
<div className="upm-help-card">
|
||||
<span className="upm-help-card__title">
|
||||
Documents we count toward billing
|
||||
</span>
|
||||
Server-side jobs: OCR, format conversion, compression, AI tools,
|
||||
automation pipelines, batch processing. Anything where Stirling's
|
||||
servers do real work on your file.
|
||||
<span className="upm-help-card__title">What we count toward billing</span>
|
||||
<ul style={{ margin: "4px 0 0", paddingLeft: 18, lineHeight: 1.55 }}>
|
||||
<li>
|
||||
<strong>Automation pipelines</strong> — chained tools or scheduled
|
||||
runs that don't need clicks
|
||||
</li>
|
||||
<li>
|
||||
<strong>AI tools</strong> — summarise, classify, redact, AI-OCR
|
||||
</li>
|
||||
<li>
|
||||
<strong>API calls</strong> — programmatic access to any Stirling
|
||||
endpoint
|
||||
</li>
|
||||
</ul>
|
||||
<div style={{ marginTop: 8, fontStyle: "italic" }}>
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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<PreviewSlot, string> = {
|
||||
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<PreviewSlot>("free");
|
||||
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
||||
const [slot, setSlot] = useState<PreviewSlot>("free-leader");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -55,7 +63,7 @@ export default function DevPaygPreview() {
|
||||
type="button"
|
||||
onClick={() => 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.
|
||||
</span>
|
||||
</header>
|
||||
<main
|
||||
style={{ maxWidth: 1100, margin: "0 auto", padding: "32px 24px" }}
|
||||
>
|
||||
{slot === "free" && (
|
||||
<EditorPlanPromo onUpgradeClick={() => setUpgradeOpen(true)} />
|
||||
)}
|
||||
{slot === "upgrade-modal" && (
|
||||
<div>
|
||||
<p style={{ color: "#666", marginBottom: 16 }}>
|
||||
The modal opens over whatever is behind it. Click below to
|
||||
re-open if you've closed it.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUpgradeOpen(true)}
|
||||
style={{
|
||||
padding: "10px 18px",
|
||||
background: "#2563eb",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Open upgrade modal
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{slot === "active-leader" && <PaygLeader />}
|
||||
{slot === "active-member" && <PaygMember />}
|
||||
<main style={{ maxWidth: 920, margin: "0 auto", padding: "32px 24px" }}>
|
||||
{slot === "free-leader" && <PaygFreeLeader />}
|
||||
{slot === "free-member" && <PaygFreeMember />}
|
||||
{slot === "subscribed-leader" && <PaygLeader />}
|
||||
{slot === "subscribed-member" && <PaygMember />}
|
||||
</main>
|
||||
<UpgradeModal
|
||||
open={upgradeOpen}
|
||||
onClose={() => 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.`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user