Skip to content
Merged
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
3 changes: 1 addition & 2 deletions docs/pro-badge-options.html
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ <h2>Feature flags (PostHog project 244066)</h2>
<div class="row">
<span class="flag"><span class="dot on"></span> <code>pro-upgrade-ui-enabled-release</code> — badge + all Pro entrypoints · ON</span>
<span class="flag"><span class="dot on"></span> <code>mobile-connect-button-enabled-release</code> — titlebar iPhone button · ON</span>
<span class="flag"><span class="dot off"></span> <code>pro-checkout-enabled-release</code> — pricing CTA → checkout · OFF</span>
</div>
<p style="margin-top:12px">Each is registered in code (owner + review date + safe default), gated live from the PostHog dashboard, and enforced by <code>scripts/lint-feature-flags.py</code> in CI against the posthog.com feature-flag-mistakes rules.</p>
<p style="margin-top:12px">Each is registered in code (owner + review date + safe default), gated live from the PostHog dashboard, and enforced by <code>scripts/lint-feature-flags.py</code> in CI against the posthog.com feature-flag-mistakes rules. Pricing checkout is launched and no longer feature-gated.</p>
</div>

<h2>In-app pricing screen (native)</h2>
Expand Down
1 change: 1 addition & 0 deletions scripts/retired-feature-flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# from code. Keys listed here must NEVER be reused for new functionality —
# reusing a stale key is how Knight Capital lost $440M in 45 minutes.
# scripts/lint-feature-flags.py fails CI if any of these reappear.
pro-checkout-enabled-release
2 changes: 1 addition & 1 deletion skills/cmux-billing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Use this skill before changing billing, pricing, Stripe, Pro entitlement, checko
## Feature Flags

- `pro-upgrade-ui-enabled-release` (PostHog id `741838`) gates all Pro UI and stays OFF in release until launch. DEBUG builds default the UI on.
- `pro-checkout-enabled-release` (PostHog id `741839`) routes the public pricing CTA.
- Public Pro and Team pricing CTAs always route through `/api/billing/checkout`; do not fall back to the download confirmation page.
- `cmux __internal_flags`, once merged, inspects and overrides flags locally.

## Prod Runbook
Expand Down
35 changes: 6 additions & 29 deletions web/app/[locale]/components/pro-cta-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,28 @@ import {
CheckoutPendingContent,
useCheckoutRedirect,
} from "../../components/checkout-navigation";
import {
isClientConfigFlagEnabled,
useClientConfigFlag,
} from "../../lib/client-config-flags";
import { FEATURE_FLAGS } from "../../lib/feature-flags";

// Single evaluation site for the pro-checkout flag (lint-enforced).
// Resolution: build-time env force (local dev / previews), then the PostHog
// flag, then the registry's safe default (the download link always works,
// including while flags are still loading — no checkout flicker).
const FORCE = process.env.NEXT_PUBLIC_CMUX_CHECKOUT_ENABLED;
const FORCED_ON = FORCE === "1" || (FORCE === undefined && process.env.NODE_ENV === "development");
const FORCED_OFF = FORCE === "0";

export function ProCtaLink({
checkoutHref,
fallbackHref,
children,
size = "default",
location = "pricing_page",
}: {
checkoutHref: string;
fallbackHref: string;
children: React.ReactNode;
size?: PricingActionSize;
location?: string;
}) {
const flagEnabled = useClientConfigFlag(FEATURE_FLAGS.proCheckout.key);
const checkout =
!FORCED_OFF &&
(FORCED_ON ||
isClientConfigFlagEnabled(
flagEnabled,
FEATURE_FLAGS.proCheckout.defaultWhenUnavailable,
));
const href = checkout ? checkoutHref : fallbackHref;
const { pending, start } = useCheckoutRedirect();
return (
<a
href={href}
href={checkoutHref}
onClick={(event) => {
posthog.capture("cmuxterm_pro_cta_clicked", { location, checkout });
// Only the checkout href is intercepted for the spinner; the download
// fallback navigates normally.
start(href, event);
posthog.capture("cmuxterm_pro_cta_clicked", {
location,
checkout: true,
});
start(checkoutHref, event);
}}
aria-busy={pending}
className={`${pricingActionClassName("primary", size)} relative`}
Expand Down
6 changes: 1 addition & 5 deletions web/app/[locale]/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ import {
} from "../../components/pricing-shared";
import { CheckoutButton } from "../../components/checkout-navigation";

// The Pro CTA destination is decided at runtime by the proCheckout PostHog
// flag inside <ProCtaLink> (see app/lib/feature-flags.ts); the download
// link is the safe fallback.
const ENTERPRISE_CTA_URL = "/enterprise";
const ANONYMOUS_IF_EXISTS = "anonymous-if-exists[deprecated]" as const;

Expand Down Expand Up @@ -152,7 +149,7 @@ export default async function PricingPage({
</SecondaryLink>
</div>
) : (
<ProCtaLink checkoutHref={PRO_CHECKOUT_URL} fallbackHref={DOWNLOAD_CONFIRMATION_HREF}>
<ProCtaLink checkoutHref={PRO_CHECKOUT_URL}>
{t("pro.cta")}
</ProCtaLink>
)}
Expand Down Expand Up @@ -216,7 +213,6 @@ export default async function PricingPage({
) : (
<ProCtaLink
checkoutHref={PRO_CHECKOUT_URL}
fallbackHref={DOWNLOAD_CONFIRMATION_HREF}
size="compact"
location="pricing_compare_header"
>
Expand Down
8 changes: 0 additions & 8 deletions web/app/lib/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,4 @@ export const FEATURE_FLAGS = {
reviewBy: "2026-10-01",
defaultWhenUnavailable: false,
},
proCheckout: {
key: "pro-checkout-enabled-release",
owner: "lawrencecchen",
description:
"Points the pricing page Pro CTA at /api/billing/checkout instead of the download link. Off until prod Stripe is live.",
reviewBy: "2026-10-01",
defaultWhenUnavailable: false,
},
} as const satisfies Record<string, FeatureFlagDefinition>;
29 changes: 29 additions & 0 deletions web/tests/pro-cta-link.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, mock, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";

const capture = mock(() => undefined);

mock.module("posthog-js", () => ({
default: { capture },
}));

const { ProCtaLink } = await import(
"../app/[locale]/components/pro-cta-link"
);

describe("Pro pricing CTA", () => {
test("routes the initial click to Stripe checkout", () => {
const html = renderToStaticMarkup(
<ProCtaLink
checkoutHref="/api/billing/checkout?plan=pro"
>
Get Pro
</ProCtaLink>,
);

expect(html).toContain(
'href="/api/billing/checkout?plan=pro"',
);
expect(html).not.toContain('href="/download/confirmation?dl=1"');
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});