| @@ -20,6 +20,14 @@ export const Route = createFileRoute("/billing_/plans")({ · 19 unmodified lines |
| 20 | 20 | component: PlansPage, |
| 21 | 21 | }); |
| 22 | 22 | |
| 23 | +const ENTERPRISE_FEATURES = [ |
| 24 | + "Self-hosted or dedicated cloud deployment support", |
| 25 | + "SSO / SAML & SCIM provisioning", |
| 26 | + "Audit logs for every tool call", |
| 27 | + "Dedicated support & onboarding", |
| 28 | + "Security reviews, DPA & SOC 2 on request", |
| 29 | +]; |
| 30 | + |
| 23 | 31 | const PLAN_META: Record<string, { tagline: string; inherits?: string; features: string[] }> = { |
| 24 | 32 | free: { |
| 25 | 33 | tagline: "For small teams getting started", |
| @@ -40,6 +48,11 @@ const PLAN_META: Record<string, { tagline: string; inherits?: string; features: · 14 unmodified lines |
| 40 | 48 | "$0.20 per 1,000 additional executions", |
| 41 | 49 | ], |
| 42 | 50 | }, |
| 51 | + enterprise: { |
| 52 | + tagline: "For orgs with custom needs", |
| 53 | + inherits: "Team", |
| 54 | + features: ENTERPRISE_FEATURES, |
| 55 | + }, |
| 43 | 56 | }; |
| 44 | 57 | |
| 45 | 58 | const ACTION_LABELS: Record<string, string> = { |
| @@ -50,28 +63,7 @@ const ACTION_LABELS: Record<string, string> = { · 4 unmodified lines |
| 50 | 63 | purchase: "Purchase", |
| 51 | 64 | }; |
| 52 | 65 | |
| 53 | | −const ENTERPRISE_FEATURES = [ |
| 54 | | − "Self-hosted or dedicated cloud deployment support", |
| 55 | | − "SSO / SAML & SCIM provisioning", |
| 56 | | − "Audit logs for every tool call", |
| 57 | | − "Dedicated support & onboarding", |
| 58 | | − "Security reviews, DPA & SOC 2 on request", |
| 59 | | −]; |
| 60 | | − |
| 61 | | −const ENTERPRISE_MAILTO = `mailto:rhys@executor.sh?subject=${encodeURIComponent( |
| 62 | | − "Executor Enterprise inquiry", |
| 63 | | −)}&body=${encodeURIComponent( |
| 64 | | − [ |
| 65 | | − "Hi,", |
| 66 | | − "", |
| 67 | | − "We're interested in Executor Enterprise.", |
| 68 | | − "", |
| 69 | | − "Company:", |
| 70 | | − "Team size:", |
| 71 | | − "Use case:", |
| 72 | | − "Requirements (SSO, self-hosted, compliance, etc.):", |
| 73 | | − ].join("\n"), |
| 74 | | −)}`; |
| 66 | +const PLAN_ORDER = ["free", "team", "enterprise"]; |
| 75 | 67 | |
| 76 | 68 | function PlansPage() { |
| 77 | 69 | const { attach, openCustomerPortal, isLoading: customerLoading } = useCustomer(); |
| @@ -80,8 +72,8 @@ function PlansPage() { · 2 unmodified lines |
| 80 | 72 | |
| 81 | 73 | const isLoading = customerLoading || plansLoading; |
| 82 | 74 | |
| 83 | | − const selfServePlans = (plans ?? ([] as Plan[])).filter( |
| 84 | | − (p: Plan) => p.id === "free" || p.id === "team", |
| 75 | + const selfServePlans = PLAN_ORDER.flatMap((id) => |
| 76 | + (plans ?? ([] as Plan[])).filter((plan: Plan) => plan.id === id), |
| 85 | 77 | ); |
| 86 | 78 | |
| 87 | 79 | return ( |
| @@ -136,6 +128,7 @@ function PlansPage() { · 48 unmodified lines |
| 136 | 128 | const isScheduled = status === "scheduled"; |
| 137 | 129 | const label = isCanceling ? "Resume" : (ACTION_LABELS[action] ?? "Select"); |
| 138 | 130 | const isUpgradeAction = action === "upgrade" || action === "activate"; |
| 131 | + const isEnterprise = plan.id === "enterprise"; |
| 139 | 132 | |
| 140 | 133 | return ( |
| 141 | 134 | <div |
| @@ -173,14 +166,14 @@ function PlansPage() { · 31 unmodified lines |
| 173 | 166 | |
| 174 | 167 | <div className="mt-4 flex items-baseline gap-1.5"> |
| 175 | 168 | <span className="text-2xl font-semibold text-foreground tabular-nums"> |
| 176 | | − ${plan.price?.amount ?? 0} |
| 169 | + {plan.id === "enterprise" ? "Custom" : `$${plan.price?.amount ?? 0}`} |
| 177 | 170 | </span> |
| 178 | | − {plan.price?.interval && ( |
| 171 | + {plan.id !== "enterprise" && plan.price?.interval && ( |
| 179 | 172 | <span className="text-sm text-muted-foreground"> |
| 180 | 173 | USD / org / {plan.price.interval} |
| 181 | 174 | </span> |
| 182 | 175 | )} |
| 183 | | − {!plan.price?.interval && ( |
| 176 | + {plan.id !== "enterprise" && !plan.price?.interval && ( |
| 184 | 177 | <span className="text-sm text-muted-foreground">USD</span> |
| 185 | 178 | )} |
| 186 | 179 | </div> |
| @@ -190,6 +183,8 @@ function PlansPage() { · 3 unmodified lines |
| 190 | 183 | <div className="flex h-9 items-center justify-center rounded-md border border-border bg-muted/30 text-sm font-medium text-muted-foreground"> |
| 191 | 184 | {isCurrent ? "Current plan" : "Scheduled"} |
| 192 | 185 | </div> |
| 186 | + ) : isEnterprise ? ( |
| 187 | + <EnterpriseContactDialog /> |
| 193 | 188 | ) : isCanceling ? ( |
| 194 | 189 | <Button |
| 195 | 190 | type="button" |
| @@ -251,48 +246,6 @@ function PlansPage() { · 55 unmodified lines |
| 251 | 246 | </div> |
| 252 | 247 | ); |
| 253 | 248 | })} |
| 254 | | − |
| 255 | | − <div className="flex flex-col rounded-xl border border-border p-5"> |
| 256 | | − <div className="flex h-6 items-center justify-between"> |
| 257 | | − <p className="text-base font-semibold text-foreground leading-none">Enterprise</p> |
| 258 | | − </div> |
| 259 | | − <p className="mt-1 text-sm text-muted-foreground">For orgs with custom needs</p> |
| 260 | | − |
| 261 | | − <div className="mt-4 flex items-baseline gap-1.5"> |
| 262 | | − <span className="text-2xl font-semibold text-foreground tabular-nums">Custom</span> |
| 263 | | − </div> |
| 264 | | − |
| 265 | | − <div className="mt-4"> |
| 266 | | − <a |
| 267 | | − href={ENTERPRISE_MAILTO} |
| 268 | | − className="flex h-9 w-full items-center justify-center rounded-md border border-border bg-background text-sm font-medium text-foreground transition-colors hover:bg-muted" |
| 269 | | − > |
| 270 | | − Contact us |
| 271 | | − </a> |
| 272 | | − </div> |
| 273 | | − |
| 274 | | − <p className="mt-5 text-xs font-medium text-foreground">Everything in Team, plus</p> |
| 275 | | − <ul role="list" className="mt-2 space-y-2"> |
| 276 | | − {ENTERPRISE_FEATURES.map((f) => ( |
| 277 | | − <li key={f} className="flex items-start gap-2 text-xs text-muted-foreground"> |
| 278 | | − <svg |
| 279 | | − viewBox="0 0 16 16" |
| 280 | | − fill="none" |
| 281 | | − className="mt-px size-3.5 shrink-0 text-primary/60" |
| 282 | | − > |
| 283 | | − <path |
| 284 | | − d="M3.5 8.5L6.5 11.5L12.5 5" |
| 285 | | − stroke="currentColor" |
| 286 | | − strokeWidth="1.5" |
| 287 | | − strokeLinecap="round" |
| 288 | | − strokeLinejoin="round" |
| 289 | | − /> |
| 290 | | − </svg> |
| 291 | | − <span>{f}</span> |
| 292 | | − </li> |
| 293 | | − ))} |
| 294 | | − </ul> |
| 295 | | − </div> |
| 296 | 249 | </div> |
| 297 | 250 | )} |
| 298 | 251 | |
| @@ -302,6 +255,48 @@ function PlansPage() { · 3 unmodified lines |
| 302 | 255 | ); |
| 303 | 256 | } |
| 304 | 257 | |
| 258 | +function EnterpriseContactDialog() { |
| 259 | + const [open, setOpen] = useState(false); |
| 260 | + |
| 261 | + return ( |
| 262 | + <Dialog open={open} onOpenChange={setOpen}> |
| 263 | + <DialogTrigger asChild> |
| 264 | + <Button |
| 265 | + type="button" |
| 266 | + variant="outline" |
| 267 | + className="flex h-9 w-full items-center justify-center rounded-md text-sm font-medium" |
| 268 | + > |
| 269 | + Contact us |
| 270 | + </Button> |
| 271 | + </DialogTrigger> |
| 272 | + <DialogContent> |
| 273 | + <DialogHeader> |
| 274 | + <DialogTitle>Talk to us about Enterprise</DialogTitle> |
| 275 | + <DialogDescription> |
| 276 | + Add{" "} |
| 277 | + <a className="font-medium text-foreground underline" href="mailto:rhys@executor.sh"> |
| 278 | + rhys@executor.sh |
| 279 | + </a>{" "} |
| 280 | + on Slack Connect and send the org name, team size, and anything you need covered. |
| 281 | + </DialogDescription> |
| 282 | + </DialogHeader> |
| 283 | + <DialogFooter> |
| 284 | + <DialogClose asChild> |
| 285 | + <Button type="button" variant="outline"> |
| 286 | + Done |
| 287 | + </Button> |
| 288 | + </DialogClose> |
| 289 | + <Button asChild> |
| 290 | + <a href="mailto:rhys@executor.sh?subject=Executor%20Enterprise%20inquiry"> |
| 291 | + Email instead |
| 292 | + </a> |
| 293 | + </Button> |
| 294 | + </DialogFooter> |
| 295 | + </DialogContent> |
| 296 | + </Dialog> |
| 297 | + ); |
| 298 | +} |
| 299 | + |
| 305 | 300 | function SlackContactCta() { |
| 306 | 301 | const [open, setOpen] = useState(false); |
| 307 | 302 | |