Skip to content

Commit c9e3470

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 c9e3470

13 files changed

Lines changed: 399 additions & 147 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/BlankStatePanels.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,10 +473,12 @@ export function BranchesNoBranches({
473473
parentEnvironment,
474474
limits,
475475
canUpgrade,
476+
showSelfServe,
476477
}: {
477478
parentEnvironment: { id: string };
478479
limits: { used: number; limit: number };
479480
canUpgrade: boolean;
481+
showSelfServe: boolean;
480482
}) {
481483
const organization = useOrganization();
482484

@@ -488,14 +490,18 @@ export function BranchesNoBranches({
488490
iconClassName="text-preview"
489491
panelClassName="max-w-full"
490492
accessory={
491-
canUpgrade ? (
493+
showSelfServe && canUpgrade ? (
492494
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
493495
Upgrade
494496
</LinkButton>
495497
) : (
496498
<Feedback
497-
button={<Button variant="primary/small">Request more</Button>}
498-
defaultValue="help"
499+
button={
500+
<Button variant={showSelfServe ? "primary/small" : "secondary/small"}>
501+
Request more
502+
</Button>
503+
}
504+
defaultValue={showSelfServe ? "help" : "enterprise"}
499505
/>
500506
)
501507
}

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: 61 additions & 16 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, getSelfServePurchaseBlockReason } 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,21 @@ 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+
const purchaseBlockReason = getSelfServePurchaseBlockReason(currentPlan);
163+
if (purchaseBlockReason === "plan_unavailable") {
164+
return json(
165+
{ ok: false, error: "Unable to verify billing status. Please try again." } as const,
166+
{ status: 503 }
167+
);
168+
}
169+
if (purchaseBlockReason === "managed_billing") {
170+
return json(
171+
{ ok: false, error: "Contact us to request more branches." } as const,
172+
{ status: 403 }
173+
);
174+
}
175+
158176
const submission = parse(formData, { schema: PurchaseSchema });
159177

160178
if (!submission.value || submission.intent !== "submit") {
@@ -237,6 +255,7 @@ export default function Page() {
237255
const environment = useEnvironment();
238256

239257
const plan = useCurrentPlan();
258+
const showSelfServe = useShowSelfServe();
240259
const requiresUpgrade =
241260
plan?.v3Subscription?.plan &&
242261
limits.used >= plan.v3Subscription.plan.limits.branches.number &&
@@ -322,6 +341,7 @@ export default function Page() {
322341
parentEnvironment={branchableEnvironment}
323342
limits={limits}
324343
canUpgrade={canUpgrade ?? false}
344+
showSelfServe={showSelfServe}
325345
/>
326346
</MainCenteredContainer>
327347
) : (
@@ -484,19 +504,26 @@ export default function Page() {
484504
planBranchLimit={planBranchLimit}
485505
/>
486506
) : canUpgrade ? (
487-
<div className="flex items-center gap-3">
488-
<Paragraph variant="small" className="whitespace-nowrap text-text-dimmed">
489-
Upgrade plan for more Preview Branches
490-
</Paragraph>
491-
<LinkButton
492-
to={v3BillingPath(organization)}
493-
variant="secondary/small"
494-
LeadingIcon={ArrowUpCircleIcon}
495-
leadingIconClassName="text-indigo-500"
496-
>
497-
Upgrade
498-
</LinkButton>
499-
</div>
507+
showSelfServe ? (
508+
<div className="flex items-center gap-3">
509+
<Paragraph variant="small" className="whitespace-nowrap text-text-dimmed">
510+
Upgrade plan for more Preview Branches
511+
</Paragraph>
512+
<LinkButton
513+
to={v3BillingPath(organization)}
514+
variant="secondary/small"
515+
LeadingIcon={ArrowUpCircleIcon}
516+
leadingIconClassName="text-indigo-500"
517+
>
518+
Upgrade
519+
</LinkButton>
520+
</div>
521+
) : (
522+
<Feedback
523+
defaultValue="enterprise"
524+
button={<Button variant="secondary/small">Request more</Button>}
525+
/>
526+
)
500527
) : null}
501528
</div>
502529
</div>
@@ -559,6 +586,7 @@ function UpgradePanel({
559586
planBranchLimit: number;
560587
}) {
561588
const organization = useOrganization();
589+
const showSelfServe = useShowSelfServe();
562590

563591
if (canPurchaseBranches && branchPricing) {
564592
return (
@@ -604,9 +632,16 @@ function UpgradePanel({
604632
</div>
605633
<DialogFooter>
606634
{canUpgrade ? (
607-
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
608-
Upgrade
609-
</LinkButton>
635+
showSelfServe ? (
636+
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
637+
Upgrade
638+
</LinkButton>
639+
) : (
640+
<Feedback
641+
defaultValue="enterprise"
642+
button={<Button variant="secondary/small">Request more</Button>}
643+
/>
644+
)
610645
) : null}
611646
</DialogFooter>
612647
</DialogContent>
@@ -632,6 +667,7 @@ function PurchaseBranchesModal({
632667
planBranchLimit: number;
633668
triggerButton?: React.ReactNode;
634669
}) {
670+
const showSelfServe = useShowSelfServe();
635671
const fetcher = useFetcher();
636672
const lastSubmission =
637673
fetcher.data && typeof fetcher.data === "object" && "intent" in fetcher.data
@@ -679,6 +715,15 @@ function PurchaseBranchesModal({
679715
const pricePerBranch = branchPricing.centsPerStep / branchPricing.stepSize / 100;
680716
const title = extraBranches === 0 ? "Purchase extra branches…" : "Add/remove extra branches…";
681717

718+
if (!showSelfServe) {
719+
return (
720+
<Feedback
721+
defaultValue="enterprise"
722+
button={<Button variant="secondary/small">Request more</Button>}
723+
/>
724+
);
725+
}
726+
682727
return (
683728
<Dialog open={open} onOpenChange={setOpen}>
684729
<DialogTrigger asChild>

0 commit comments

Comments
 (0)