g1t/apps/web/app/components/security-activation.tsx
| 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 | */ |
| 7 | import { CreditCard, ShieldCheck } from "lucide-react"; |
| 8 | import { Form } from "react-router"; |
| 9 | |
| 10 | import type { FeatureState } from "@g1t/contracts"; |
| 11 | |
| 12 | import { SubmitButton } from "./ui"; |
| 13 | |
| 14 | export 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 | } |