Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .server-changes/hide-self-serve-billing-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
area: webapp
type: improvement
---

Hide self-serve billing and upgrade UI for organizations that are billed
directly. When self-serve is off, plan pickers, upgrade buttons, billing
alerts, and seat/branch purchase flows are replaced with a "Contact us"
option, and the billing page shows a managed-billing message instead.
12 changes: 9 additions & 3 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,12 @@ export function BranchesNoBranches({
parentEnvironment,
limits,
canUpgrade,
showSelfServe,
}: {
parentEnvironment: { id: string };
limits: { used: number; limit: number };
canUpgrade: boolean;
showSelfServe: boolean;
}) {
const organization = useOrganization();

Expand All @@ -488,14 +490,18 @@ export function BranchesNoBranches({
iconClassName="text-preview"
panelClassName="max-w-full"
accessory={
canUpgrade ? (
showSelfServe && canUpgrade ? (
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
Upgrade
</LinkButton>
) : (
<Feedback
button={<Button variant="primary/small">Request more</Button>}
defaultValue="help"
button={
<Button variant={showSelfServe ? "primary/small" : "secondary/small"}>
Request more
</Button>
}
defaultValue={showSelfServe ? "help" : "enterprise"}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { LinkButton } from "../primitives/Buttons";
import { HelpAndFeedback } from "./HelpAndFeedbackPopover";
import { SideMenuHeader } from "./SideMenuHeader";
import { SideMenuItem } from "./SideMenuItem";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { Paragraph } from "../primitives/Paragraph";
import { Badge } from "../primitives/Badge";
Expand All @@ -56,6 +57,7 @@ export function OrganizationSettingsSideMenu({
const { isManagedCloud } = useFeatures();
const featureFlags = useFeatureFlags();
const currentPlan = useCurrentPlan();
const showSelfServe = useShowSelfServe();
const isAdmin = useHasAdminAccess();
const showBuildInfo = isAdmin || !isManagedCloud;

Expand Down Expand Up @@ -104,14 +106,16 @@ export function OrganizationSettingsSideMenu({
) : undefined
}
/>
<SideMenuItem
name="Billing alerts"
icon={BellAlertIcon}
activeIconColor="text-rose-500"
inactiveIconColor="text-rose-500"
to={v3BillingAlertsPath(organization)}
data-action="billing-alerts"
/>
{showSelfServe ? (
<SideMenuItem
name="Billing alerts"
icon={BellAlertIcon}
activeIconColor="text-rose-500"
inactiveIconColor="text-rose-500"
to={v3BillingAlertsPath(organization)}
data-action="billing-alerts"
/>
) : null}
</>
)}
{featureFlags.hasPrivateConnections && (
Expand Down
7 changes: 7 additions & 0 deletions apps/webapp/app/hooks/useShowSelfServe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";

/** Whether the org should see self-serve billing UI (plan picker, Stripe checkout, upgrades). */
export function useShowSelfServe(): boolean {
const plan = useCurrentPlan();
return plan?.v3Subscription?.showSelfServe ?? true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import assertNever from "assert-never";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { AlertsNoneDev, AlertsNoneDeployed } from "~/components/BlankStatePanels";
import { Feedback } from "~/components/Feedback";
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
import { EnabledStatus } from "~/components/runs/v3/EnabledStatus";
import { prisma } from "~/db.server";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { redirectWithSuccessMessage } from "~/models/message.server";
Expand Down Expand Up @@ -182,6 +184,7 @@ export default function Page() {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
const showSelfServe = useShowSelfServe();

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

Expand Down Expand Up @@ -343,9 +346,16 @@ export default function Page() {
</Header3>
)}

<LinkButton to={v3BillingPath(organization)} variant="secondary/small">
Upgrade
</LinkButton>
{showSelfServe ? (
<LinkButton to={v3BillingPath(organization)} variant="secondary/small">
Upgrade
</LinkButton>
) : (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
)}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons";
import { BranchesNoBranchableEnvironment, BranchesNoBranches } from "~/components/BlankStatePanels";
import { Feedback } from "~/components/Feedback";
import { GitMetadata } from "~/components/GitMetadata";
import { V4Title } from "~/components/V4Badge";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
Expand Down Expand Up @@ -56,13 +57,15 @@ import {
} from "~/components/primitives/Table";
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";

import { findProjectBySlug } from "~/models/project.server";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { BranchesPresenter } from "~/presenters/v3/BranchesPresenter.server";
import { logger } from "~/services/logger.server";
import { getCurrentPlan, getSelfServePurchaseBlockReason } from "~/services/platform.v3.server";
import { requireUserId } from "~/services/session.server";
import { UpsertBranchService } from "~/services/upsertBranch.server";
import { cn } from "~/utils/cn";
Expand Down Expand Up @@ -155,6 +158,21 @@ export async function action({ request, params }: ActionFunctionArgs) {
throw redirectWithErrorMessage(redirectPath, request, "Project not found");
}

const currentPlan = await getCurrentPlan(project.organizationId);
const purchaseBlockReason = getSelfServePurchaseBlockReason(currentPlan);
if (purchaseBlockReason === "plan_unavailable") {
return json(
{ ok: false, error: "Unable to verify billing status. Please try again." } as const,
{ status: 503 }
);
}
if (purchaseBlockReason === "managed_billing") {
return json(
{ ok: false, error: "Contact us to request more branches." } as const,
{ status: 403 }
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const submission = parse(formData, { schema: PurchaseSchema });

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

const plan = useCurrentPlan();
const showSelfServe = useShowSelfServe();
const requiresUpgrade =
plan?.v3Subscription?.plan &&
limits.used >= plan.v3Subscription.plan.limits.branches.number &&
Expand Down Expand Up @@ -322,6 +341,7 @@ export default function Page() {
parentEnvironment={branchableEnvironment}
limits={limits}
canUpgrade={canUpgrade ?? false}
showSelfServe={showSelfServe}
/>
</MainCenteredContainer>
) : (
Expand Down Expand Up @@ -484,19 +504,26 @@ export default function Page() {
planBranchLimit={planBranchLimit}
/>
) : canUpgrade ? (
<div className="flex items-center gap-3">
<Paragraph variant="small" className="whitespace-nowrap text-text-dimmed">
Upgrade plan for more Preview Branches
</Paragraph>
<LinkButton
to={v3BillingPath(organization)}
variant="secondary/small"
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="text-indigo-500"
>
Upgrade
</LinkButton>
</div>
showSelfServe ? (
<div className="flex items-center gap-3">
<Paragraph variant="small" className="whitespace-nowrap text-text-dimmed">
Upgrade plan for more Preview Branches
</Paragraph>
<LinkButton
to={v3BillingPath(organization)}
variant="secondary/small"
LeadingIcon={ArrowUpCircleIcon}
leadingIconClassName="text-indigo-500"
>
Upgrade
</LinkButton>
</div>
) : (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
)
) : null}
</div>
</div>
Expand Down Expand Up @@ -559,6 +586,7 @@ function UpgradePanel({
planBranchLimit: number;
}) {
const organization = useOrganization();
const showSelfServe = useShowSelfServe();

if (canPurchaseBranches && branchPricing) {
return (
Expand Down Expand Up @@ -604,9 +632,16 @@ function UpgradePanel({
</div>
<DialogFooter>
{canUpgrade ? (
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
Upgrade
</LinkButton>
showSelfServe ? (
<LinkButton variant="primary/small" to={v3BillingPath(organization)}>
Upgrade
</LinkButton>
) : (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
)
) : null}
</DialogFooter>
</DialogContent>
Expand All @@ -632,6 +667,7 @@ function PurchaseBranchesModal({
planBranchLimit: number;
triggerButton?: React.ReactNode;
}) {
const showSelfServe = useShowSelfServe();
const fetcher = useFetcher();
const lastSubmission =
fetcher.data && typeof fetcher.data === "object" && "intent" in fetcher.data
Expand Down Expand Up @@ -679,6 +715,15 @@ function PurchaseBranchesModal({
const pricePerBranch = branchPricing.centsPerStep / branchPricing.stepSize / 100;
const title = extraBranches === 0 ? "Purchase extra branches…" : "Add/remove extra branches…";

if (!showSelfServe) {
return (
<Feedback
defaultValue="enterprise"
button={<Button variant="secondary/small">Request more</Button>}
/>
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
kathiekiwi marked this conversation as resolved.

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
Expand Down
Loading
Loading