Skip to content

g1t/apps/web/app/components/security-activation.tsx

87 lines3,746 bytesCodeBlame
1/**
2 * The Security and quality activation on the Billing page: what it turns
3 * on, its price (the price book's, through billing), and starting or
4 * ending it. Owners only; comped and enterprise workspaces have it
5 * included.
6 */
7import { CreditCard, ShieldCheck } from "lucide-react";
8import { Form } from "react-router";
9
10import type { FeatureState } from "@g1t/contracts";
11
12import { SubmitButton } from "./ui";
13
14export function SecurityActivationCard({
15 state,
16 owner,
17 enabled,
18 error,
19}: {
20 state: FeatureState | null;
21 owner: boolean;
22 enabled: boolean;
23 error?: string;
24}) {
25 if (!state) return null;
26 const { plan, subscription, included } = state;
27 const status = subscription?.status;
28 const on = state.on;
29 const label = included ? "Included" : status === "canceling" ? "Ends at the period's end" : status === "past_due" ? "Payment due" : on ? "On" : "Off";
30 return (
31 <section id="security" className="mb-6 rounded-xl border border-line bg-surface p-5">
32 <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
33 <div className="min-w-0">
34 <h2 className="flex flex-wrap items-center gap-2 text-base font-semibold">
35 <ShieldCheck size={16} className="text-accent" />
36 {plan.title}
37 <span className={`rounded-full px-2 py-0.5 text-xs ${on ? "bg-success/15 text-success" : "border border-line text-muted"}`}>{label}</span>
38 </h2>
39 <p className="mt-1.5 max-w-2xl text-sm text-muted">
40 The security suite's paid features for the workspace's private repositories. Public repositories have them free, and
41 secret scanning, push protection, vulnerability alerts and security updates stay free everywhere.
42 </p>
43 </div>
44 <p className="shrink-0 sm:text-right">
45 <span className="text-2xl font-semibold tabular-nums tracking-tight">${(plan.monthlyCents / 100).toFixed(plan.monthlyCents % 100 ? 2 : 0)}</span>
46 <span className="text-sm text-muted"> / month</span>
47 <span className="block text-xs text-faint">per workspace, never per seat</span>
48 </p>
49 </div>
50 <ul className="mt-4 grid gap-1.5 text-sm text-muted sm:grid-cols-2">
51 {plan.includes.map((line) => (
52 <li key={line} className="flex gap-2">
53 <span className="text-success">✓</span>
54 {line}
55 </li>
56 ))}
57 </ul>
58 <p className="mt-3 text-xs text-faint">{plan.overage}</p>
59 {included ? (
60 <p className="mt-4 text-sm text-muted">Included for this workspace at no charge.</p>
61 ) : !enabled ? (
62 <p className="mt-4 text-sm text-muted">Payments are not set up on this g1t, so it is on for every workspace.</p>
63 ) : !owner ? (
64 <p className="mt-4 text-sm text-muted">An owner turns it on or off.</p>
65 ) : (
66 <Form method="post" className="mt-5 flex flex-wrap items-center gap-3">
67 <input type="hidden" name="feature" value="security" />
68 {!on ? (
69 <SubmitButton variant="accent" name="intent" value="subscribe" pending="Opening Stripe…">
70 <CreditCard size={14} />
71 Turn on Security and quality
72 </SubmitButton>
73 ) : status === "canceling" ? (
74 <SubmitButton variant="accent" name="intent" value="resume" pending="Saving…">
75 Keep it on
76 </SubmitButton>
77 ) : (
78 <SubmitButton variant="quiet" name="intent" value="cancel" pending="Saving…">
79 Turn off at the period's end
80 </SubmitButton>
81 )}
82 </Form>
83 )}
84 {error && <p className="mt-3 text-sm text-danger">{error}</p>}
85 </section>
86 );
87}