Skip to content

Commit 530b388

Browse files
authored
feat(webapp): hide self-serve billing UI for managed-billing orgs (#3898)
### Summary Self-serve billing UI is now hidden for managed-billing organizations. Plan pickers, upgrade actions, billing alerts, and related upgrade prompts are replaced with a "Contact us" option where appropriate. Uses the new showSelfServe subscription flag, defaulting to true for existing self-serve organizations. ### Testing - [x] billing pages render correctly for self-serve organizations. - [x] managed-billing organizations no longer see self-serve upgrade flows. - [x] "Contact us" actions are shown instead of upgrade actions where applicable. ### Changelog Hide self-serve billing flows for managed-billing organizations behind the new showSelfServe subscription flag.
1 parent 1cf56e5 commit 530b388

18 files changed

Lines changed: 586 additions & 256 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Hide self-serve billing and upgrade options for directly-billed organizations; show Contact us instead.

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ export function NoWaitpointTokens() {
434434
);
435435
}
436436

437-
export function BranchesNoBranchableEnvironment() {
437+
export function BranchesNoBranchableEnvironment({ showSelfServe }: { showSelfServe: boolean }) {
438438
const { isManagedCloud } = useFeatures();
439439
const organization = useOrganization();
440440

@@ -462,9 +462,16 @@ export function BranchesNoBranchableEnvironment() {
462462
iconClassName="text-preview"
463463
panelClassName="max-w-full"
464464
accessory={
465-
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
466-
Upgrade
467-
</LinkButton>
465+
showSelfServe ? (
466+
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
467+
Upgrade
468+
</LinkButton>
469+
) : (
470+
<Feedback
471+
button={<Button variant="secondary/small">Request more</Button>}
472+
defaultValue="enterprise"
473+
/>
474+
)
468475
}
469476
>
470477
<Paragraph spacing variant="small">
@@ -483,10 +490,12 @@ export function BranchesNoBranches({
483490
parentEnvironment,
484491
limits,
485492
canUpgrade,
493+
showSelfServe,
486494
}: {
487495
parentEnvironment: { id: string };
488496
limits: { used: number; limit: number };
489497
canUpgrade: boolean;
498+
showSelfServe: boolean;
490499
}) {
491500
const organization = useOrganization();
492501

@@ -498,14 +507,18 @@ export function BranchesNoBranches({
498507
iconClassName="text-preview"
499508
panelClassName="max-w-full"
500509
accessory={
501-
canUpgrade ? (
510+
showSelfServe && canUpgrade ? (
502511
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
503512
Upgrade
504513
</LinkButton>
505514
) : (
506515
<Feedback
507-
button={<Button variant="primary/small">Request more</Button>}
508-
defaultValue="help"
516+
button={
517+
<Button variant={showSelfServe ? "primary/small" : "secondary/small"}>
518+
Request more
519+
</Button>
520+
}
521+
defaultValue={showSelfServe ? "help" : "enterprise"}
509522
/>
510523
)
511524
}

apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { LinkButton } from "../primitives/Buttons";
2929
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
3030
import { SideMenuHeader } from "./SideMenuHeader";
3131
import { SideMenuItem } from "./SideMenuItem";
32+
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
3233
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
3334
import { Paragraph } from "../primitives/Paragraph";
3435
import { Badge } from "../primitives/Badge";
@@ -55,6 +56,7 @@ export function OrganizationSettingsSideMenu({
5556
const { isManagedCloud } = useFeatures();
5657
const featureFlags = useFeatureFlags();
5758
const currentPlan = useCurrentPlan();
59+
const showSelfServe = useShowSelfServe();
5860
const isAdmin = useHasAdminAccess();
5961
const showBuildInfo = isAdmin || !isManagedCloud;
6062

@@ -103,14 +105,16 @@ export function OrganizationSettingsSideMenu({
103105
) : undefined
104106
}
105107
/>
106-
<SideMenuItem
107-
name="Billing alerts"
108-
icon={BellIcon}
109-
activeIconColor="text-text-bright"
110-
inactiveIconColor="text-text-dimmed"
111-
to={v3BillingAlertsPath(organization)}
112-
data-action="billing-alerts"
113-
/>
108+
{showSelfServe ? (
109+
<SideMenuItem
110+
name="Billing alerts"
111+
icon={BellIcon}
112+
activeIconColor="text-text-bright"
113+
inactiveIconColor="text-text-dimmed"
114+
to={v3BillingAlertsPath(organization)}
115+
data-action="billing-alerts"
116+
/>
117+
) : null}
114118
</>
115119
)}
116120
<SideMenuItem

apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { EnvelopeIcon } from "@heroicons/react/20/solid";
44
import { DialogClose } from "@radix-ui/react-dialog";
55
import { useFetcher } from "@remix-run/react";
66
import { type ReactNode, useEffect, useState } from "react";
7+
import { Feedback } from "~/components/Feedback";
78
import { Button } from "~/components/primitives/Buttons";
89
import {
910
Dialog,
@@ -20,6 +21,7 @@ import { InputNumberStepper } from "~/components/primitives/InputNumberStepper";
2021
import { Label } from "~/components/primitives/Label";
2122
import { Paragraph } from "~/components/primitives/Paragraph";
2223
import { SpinnerWhite } from "~/components/primitives/Spinner";
24+
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
2325
import { PurchaseSchema } from "~/routes/resources.orgs.$organizationSlug.schedules-addon";
2426
import { cn } from "~/utils/cn";
2527
import { formatCurrency, formatNumber } from "~/utils/numberFormatter";
@@ -49,6 +51,7 @@ export function PurchaseSchedulesModal({
4951
planScheduleLimit,
5052
triggerButton,
5153
}: Props) {
54+
const showSelfServe = useShowSelfServe();
5255
const fetcher = useFetcher();
5356
const lastSubmission =
5457
fetcher.data && typeof fetcher.data === "object" && "intent" in fetcher.data
@@ -105,6 +108,15 @@ export function PurchaseSchedulesModal({
105108
const stepUnit = formatNumber(stepSize);
106109
const title = extraSchedules === 0 ? "Purchase extra schedules…" : "Add/remove extra schedules…";
107110

111+
if (!showSelfServe) {
112+
return (
113+
<Feedback
114+
defaultValue="enterprise"
115+
button={<Button variant="secondary/small">Request more</Button>}
116+
/>
117+
);
118+
}
119+
108120
return (
109121
<Dialog open={open} onOpenChange={setOpen}>
110122
<DialogTrigger asChild>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ArrowUpCircleIcon } from "@heroicons/react/20/solid";
2+
import { Feedback } from "~/components/Feedback";
3+
import { Button, LinkButton } from "~/components/primitives/Buttons";
4+
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
5+
import { type MatchedOrganization } from "~/hooks/useOrganizations";
6+
import { v3BillingPath } from "~/utils/pathBuilder";
7+
import { PurchaseSchedulesModal, type SchedulePricing } from "./PurchaseSchedulesModal";
8+
9+
type Props = {
10+
actionPath: string;
11+
canPurchaseSchedules: boolean;
12+
schedulePricing: SchedulePricing | null;
13+
extraSchedules: number;
14+
limits: { used: number; limit: number };
15+
maxScheduleQuota: number;
16+
planScheduleLimit: number;
17+
canUpgrade: boolean;
18+
organization: MatchedOrganization;
19+
variant?: "dialog" | "banner";
20+
};
21+
22+
export function ScheduleLimitActions({
23+
actionPath,
24+
canPurchaseSchedules,
25+
schedulePricing,
26+
extraSchedules,
27+
limits,
28+
maxScheduleQuota,
29+
planScheduleLimit,
30+
canUpgrade,
31+
organization,
32+
variant = "banner",
33+
}: Props) {
34+
const showSelfServe = useShowSelfServe();
35+
36+
if (!showSelfServe) {
37+
return (
38+
<Feedback
39+
button={<Button variant="secondary/small">Request more</Button>}
40+
defaultValue="enterprise"
41+
/>
42+
);
43+
}
44+
45+
if (canPurchaseSchedules && schedulePricing) {
46+
return (
47+
<PurchaseSchedulesModal
48+
actionPath={actionPath}
49+
schedulePricing={schedulePricing}
50+
extraSchedules={extraSchedules}
51+
usedSchedules={limits.used}
52+
maxQuota={maxScheduleQuota}
53+
planScheduleLimit={planScheduleLimit}
54+
triggerButton={
55+
variant === "dialog" ? (
56+
<Button variant="primary/small">Purchase more…</Button>
57+
) : undefined
58+
}
59+
/>
60+
);
61+
}
62+
63+
if (canUpgrade) {
64+
return variant === "dialog" ? (
65+
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
66+
Upgrade
67+
</LinkButton>
68+
) : (
69+
<LinkButton
70+
to={v3BillingPath(organization)}
71+
variant="secondary/small"
72+
LeadingIcon={ArrowUpCircleIcon}
73+
leadingIconClassName="text-indigo-500"
74+
>
75+
Upgrade
76+
</LinkButton>
77+
);
78+
}
79+
80+
return (
81+
<Feedback button={<Button variant="primary/small">Request more</Button>} defaultValue="help" />
82+
);
83+
}

apps/webapp/app/components/schedules/SchedulesUsageBar.tsx

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { ArrowUpCircleIcon } from "@heroicons/react/20/solid";
2-
import { Feedback } from "~/components/Feedback";
3-
import { Button, LinkButton } from "~/components/primitives/Buttons";
41
import { Header3 } from "~/components/primitives/Headers";
52
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
63
import { useOrganization } from "~/hooks/useOrganizations";
7-
import { v3BillingPath, v3SchedulesAddOnPath } from "~/utils/pathBuilder";
8-
import {
9-
PurchaseSchedulesModal,
10-
type SchedulePricing,
11-
} from "./PurchaseSchedulesModal";
4+
import { v3SchedulesAddOnPath } from "~/utils/pathBuilder";
5+
import { ScheduleLimitActions } from "./ScheduleLimitActions";
6+
import { type SchedulePricing } from "./PurchaseSchedulesModal";
127

138
type Props = {
149
limits: { used: number; limit: number };
@@ -81,30 +76,17 @@ export function SchedulesUsageBar({
8176
</div>
8277
)}
8378

84-
{canPurchaseSchedules && schedulePricing ? (
85-
<PurchaseSchedulesModal
86-
actionPath={actionPath}
87-
schedulePricing={schedulePricing}
88-
extraSchedules={extraSchedules}
89-
usedSchedules={limits.used}
90-
maxQuota={maxScheduleQuota}
91-
planScheduleLimit={planScheduleLimit}
92-
/>
93-
) : canUpgrade ? (
94-
<LinkButton
95-
to={v3BillingPath(organization)}
96-
variant="secondary/small"
97-
LeadingIcon={ArrowUpCircleIcon}
98-
leadingIconClassName="text-indigo-500"
99-
>
100-
Upgrade
101-
</LinkButton>
102-
) : (
103-
<Feedback
104-
button={<Button variant="secondary/small">Request more</Button>}
105-
defaultValue="help"
106-
/>
107-
)}
79+
<ScheduleLimitActions
80+
actionPath={actionPath}
81+
canPurchaseSchedules={canPurchaseSchedules}
82+
schedulePricing={schedulePricing}
83+
extraSchedules={extraSchedules}
84+
limits={limits}
85+
maxScheduleQuota={maxScheduleQuota}
86+
planScheduleLimit={planScheduleLimit}
87+
canUpgrade={canUpgrade}
88+
organization={organization}
89+
/>
10890
</div>
10991
</div>
11092
</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
2+
3+
/** Whether the org should see self-serve billing UI (plan picker, Stripe checkout, upgrades). */
4+
export function useShowSelfServe(): boolean {
5+
const plan = useCurrentPlan();
6+
return plan?.v3Subscription?.showSelfServe ?? true;
7+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import assertNever from "assert-never";
1818
import { typedjson, useTypedLoaderData } from "remix-typedjson";
1919
import { z } from "zod";
2020
import { AlertsNoneDev, AlertsNoneDeployed } from "~/components/BlankStatePanels";
21+
import { Feedback } from "~/components/Feedback";
2122
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
2223
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
2324
import { Button, LinkButton } from "~/components/primitives/Buttons";
@@ -45,6 +46,7 @@ import {
4546
import { EnabledStatus } from "~/components/runs/v3/EnabledStatus";
4647
import { prisma } from "~/db.server";
4748
import { useEnvironment } from "~/hooks/useEnvironment";
49+
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
4850
import { useOrganization } from "~/hooks/useOrganizations";
4951
import { useProject } from "~/hooks/useProject";
5052
import { redirectWithSuccessMessage } from "~/models/message.server";
@@ -182,6 +184,7 @@ export default function Page() {
182184
const organization = useOrganization();
183185
const project = useProject();
184186
const environment = useEnvironment();
187+
const showSelfServe = useShowSelfServe();
185188

186189
const requiresUpgrade = limits.used >= limits.limit;
187190

@@ -343,9 +346,16 @@ export default function Page() {
343346
</Header3>
344347
)}
345348

346-
<LinkButton to={v3BillingPath(organization)} variant="secondary/small">
347-
Upgrade
348-
</LinkButton>
349+
{showSelfServe ? (
350+
<LinkButton to={v3BillingPath(organization)} variant="secondary/small">
351+
Upgrade
352+
</LinkButton>
353+
) : (
354+
<Feedback
355+
defaultValue="enterprise"
356+
button={<Button variant="secondary/small">Request more</Button>}
357+
/>
358+
)}
349359
</div>
350360
</div>
351361
</div>

0 commit comments

Comments
 (0)