File size: 5,679 Bytes
00a4363
 
 
 
 
 
 
 
 
 
d149013
 
79ca2b4
00a4363
 
 
d149013
 
 
 
00a4363
 
 
 
 
 
 
 
66de9c8
00a4363
66de9c8
 
 
 
 
00a4363
 
 
 
 
 
79ca2b4
00a4363
66de9c8
 
070c931
66de9c8
 
 
00a4363
 
 
 
 
 
 
79ca2b4
00a4363
66de9c8
 
070c931
66de9c8
 
 
 
00a4363
 
 
 
aeea10c
d149013
 
 
79ca2b4
 
 
 
 
aeea10c
79ca2b4
 
 
 
 
 
d149013
 
aeea10c
d149013
 
 
00a4363
 
 
66de9c8
79ca2b4
00a4363
 
 
 
c0b9f7d
00a4363
 
 
 
 
 
 
 
 
d149013
 
 
 
 
 
79ca2b4
d149013
 
 
00a4363
 
 
d149013
 
00a4363
 
 
 
 
 
d149013
 
00a4363
 
d149013
 
 
00a4363
 
 
d149013
 
 
00a4363
 
 
 
 
 
 
 
aeea10c
 
 
 
d149013
00a4363
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api } from "../api/client";
import { CheckIcon } from "./icons/Icons";

type Plan = {
  id: string;
  name: string;
  tagline: string;
  price_monthly_usd: number;
  price_annual_monthly_usd?: number;
  price_annual_usd?: number;
  annual_savings_pct?: number;
  features: string[];
};

type Cycle = "monthly" | "annual";

const ANNUAL_DISCOUNT = 0.25;

// Mirrors backend app/services/plans.py so the page renders before (or without) the API.
const FALLBACK: Plan[] = [
  {
    id: "free",
    name: "Free",
    tagline: "For trying Notestack on your archive",
    price_monthly_usd: 0,
    features: [
      "Up to 5 posts indexed, from any source",
      "Grounded research chat with citations",
      "1 audio overview, any length",
      "1 video",
      "2 Launch Kits",
      "2 infographics",
      "2 reports a month",
    ],
  },
  {
    id: "writer",
    name: "Writer",
    tagline: "For writers publishing every week",
    price_monthly_usd: 24.99,
    features: [
      "Up to 100 posts indexed, from any source",
      "10x more audio overviews than Free (60 min a month)",
      "10 videos a month",
      "20 Launch Kits a month",
      "30 reports a month",
      "30 infographics a month",
      "Voice cloning with consent",
    ],
  },
  {
    id: "studio",
    name: "Studio",
    tagline: "For publications and power users",
    price_monthly_usd: 48.99,
    features: [
      "Up to 250 posts indexed, from any source",
      "40x more audio overviews than Free (240 min a month)",
      "20 videos a month",
      "50 Launch Kits a month",
      "Unlimited reports",
      "Unlimited infographics",
      "Voice cloning with consent",
    ],
  },
];

export function money(n: number): string {
  return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`;
}

/** Nearest .99 price, mirroring plans.to_99 on the backend. */
function to99(amount: number): number {
  return amount <= 0 ? 0 : Math.max(0.99, Math.round(amount + 0.01) - 0.01);
}

export function annualMonthly(plan: Plan): number {
  return plan.price_annual_monthly_usd ?? to99(plan.price_monthly_usd * (1 - ANNUAL_DISCOUNT));
}

function savingsPct(plan: Plan): number {
  if (!plan.price_monthly_usd) return 0;
  return plan.annual_savings_pct ?? Math.round((1 - annualMonthly(plan) / plan.price_monthly_usd) * 100);
}

export function annualTotal(plan: Plan): number {
  return plan.price_annual_usd ?? Math.round(annualMonthly(plan) * 12 * 100) / 100;
}

export default function PricingTiers() {
  const [plans, setPlans] = useState<Plan[]>(FALLBACK);
  const [billingEnabled, setBillingEnabled] = useState(false);
  const [cycle, setCycle] = useState<Cycle>("annual");
  const bestSaving = Math.max(0, ...plans.map(savingsPct));

  useEffect(() => {
    api<{ billing_enabled: boolean; plans: Plan[] }>("/api/billing/plans")
      .then((r) => {
        if (Array.isArray(r.plans) && r.plans.length > 0) setPlans(r.plans);
        setBillingEnabled(r.billing_enabled);
      })
      .catch(() => {
        /* keep fallback */
      });
  }, []);

  return (
    <div className="pricing">
      <div className="pricing-top">
        <div className="cycle-toggle" role="radiogroup" aria-label="Billing cycle">
          <button type="button" role="radio" aria-checked={cycle === "monthly"} className={cycle === "monthly" ? "on" : ""} onClick={() => setCycle("monthly")}>
            Monthly
          </button>
          <button type="button" role="radio" aria-checked={cycle === "annual"} className={cycle === "annual" ? "on" : ""} onClick={() => setCycle("annual")}>
            Annual {bestSaving > 0 && <span className="save mono">Save {bestSaving}%</span>}
          </button>
        </div>
      </div>
      <div className="pricing-grid">
        {plans.map((plan) => {
          const featured = plan.id === "writer";
          const free = plan.price_monthly_usd === 0;
          const annual = cycle === "annual" && !free;
          return (
            <article key={plan.id} className={`card tier${featured ? " tier-featured active" : ""}`}>
              {featured && <span className="badge tier-flag">Most popular</span>}
              <h3>{plan.name}</h3>
              <p className="muted tier-tagline">{plan.tagline}</p>
              <p className="tier-price">
                {free ? (
                  <span className="tier-amount">Free</span>
                ) : (
                  <>
                    {annual && <span className="tier-was muted">{money(plan.price_monthly_usd)}</span>}
                    <span className="tier-amount">{money(annual ? annualMonthly(plan) : plan.price_monthly_usd)}</span>
                    <span className="muted">/month</span>
                  </>
                )}
              </p>
              <p className="mono muted tier-billing">
                {free ? "Free forever" : annual ? `${money(annualTotal(plan))} billed yearly` : "Billed monthly"}
              </p>
              <ul className="tier-features">
                {plan.features.map((f) => (
                  <li key={f}>
                    <CheckIcon size={16} />
                    <span>{f}</span>
                  </li>
                ))}
              </ul>
              <Link
                to={billingEnabled && !free ? `/app?upgrade=${plan.id}` : "/auth?mode=signup"}
                className={`btn ${featured ? "btn-primary" : ""} tier-cta`}
              >
                {billingEnabled && !free ? `Choose ${plan.name}` : "Start free"}
              </Link>
            </article>
          );
        })}
      </div>
    </div>
  );
}