Skip to content

Commit 6abeffd

Browse files
committed
feat(webapp): hide self-serve billing UI for managed-billing orgs
Self-serve billing UI is now hidden for managed-billing organizations. Uses the new `showSelfServe` subscription flag, defaulting to `true` for existing self-serve organizations.
1 parent ef04cc3 commit 6abeffd

11 files changed

Lines changed: 286 additions & 119 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Hide self-serve billing and upgrade UI for organizations that are billed
7+
directly. When self-serve is off, plan pickers, upgrade buttons, billing
8+
alerts, and seat/branch purchase flows are replaced with a "Contact us"
9+
option, and the billing page shows a managed-billing message instead.

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

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

@@ -104,14 +106,16 @@ export function OrganizationSettingsSideMenu({
104106
) : undefined
105107
}
106108
/>
107-
<SideMenuItem
108-
name="Billing alerts"
109-
icon={BellAlertIcon}
110-
activeIconColor="text-rose-500"
111-
inactiveIconColor="text-rose-500"
112-
to={v3BillingAlertsPath(organization)}
113-
data-action="billing-alerts"
114-
/>
109+
{showSelfServe ? (
110+
<SideMenuItem
111+
name="Billing alerts"
112+
icon={BellAlertIcon}
113+
activeIconColor="text-rose-500"
114+
inactiveIconColor="text-rose-500"
115+
to={v3BillingAlertsPath(organization)}
116+
data-action="billing-alerts"
117+
/>
118+
) : null}
115119
</>
116120
)}
117121
{featureFlags.hasPrivateConnections && (
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>

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
1212
import { z } from "zod";
1313
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
1414
import { BranchesNoBranchableEnvironment, BranchesNoBranches } from "~/components/BlankStatePanels";
15+
import { Feedback } from "~/components/Feedback";
1516
import { GitMetadata } from "~/components/GitMetadata";
1617
import { V4Title } from "~/components/V4Badge";
1718
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
@@ -56,13 +57,15 @@ import {
5657
} from "~/components/primitives/Table";
5758
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
5859
import { useEnvironment } from "~/hooks/useEnvironment";
60+
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
5961
import { useOrganization } from "~/hooks/useOrganizations";
6062
import { useProject } from "~/hooks/useProject";
6163

6264
import { findProjectBySlug } from "~/models/project.server";
6365
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
6466
import { BranchesPresenter } from "~/presenters/v3/BranchesPresenter.server";
6567
import { logger } from "~/services/logger.server";
68+
import { getCurrentPlan } from "~/services/platform.v3.server";
6669
import { requireUserId } from "~/services/session.server";
6770
import { UpsertBranchService } from "~/services/upsertBranch.server";
6871
import { cn } from "~/utils/cn";
@@ -155,6 +158,14 @@ export async function action({ request, params }: ActionFunctionArgs) {
155158
throw redirectWithErrorMessage(redirectPath, request, "Project not found");
156159
}
157160

161+
const currentPlan = await getCurrentPlan(project.organizationId);
162+
if (currentPlan?.v3Subscription?.showSelfServe === false) {
163+
return json(
164+
{ ok: false, error: "Contact us to request more branches." } as const,
165+
{ status: 403 }
166+
);
167+
}
168+
158169
const submission = parse(formData, { schema: PurchaseSchema });
159170

160171
if (!submission.value || submission.intent !== "submit") {
@@ -632,6 +643,7 @@ function PurchaseBranchesModal({
632643
planBranchLimit: number;
633644
triggerButton?: React.ReactNode;
634645
}) {
646+
const showSelfServe = useShowSelfServe();
635647
const fetcher = useFetcher();
636648
const lastSubmission =
637649
fetcher.data && typeof fetcher.data === "object" && "intent" in fetcher.data
@@ -679,6 +691,15 @@ function PurchaseBranchesModal({
679691
const pricePerBranch = branchPricing.centsPerStep / branchPricing.stepSize / 100;
680692
const title = extraBranches === 0 ? "Purchase extra branches…" : "Add/remove extra branches…";
681693

694+
if (!showSelfServe) {
695+
return (
696+
<Feedback
697+
defaultValue="enterprise"
698+
button={<Button variant="secondary/small">Request more</Button>}
699+
/>
700+
);
701+
}
702+
682703
return (
683704
<Dialog open={open} onOpenChange={setOpen}>
684705
<DialogTrigger asChild>

0 commit comments

Comments
 (0)