Repository navigation
Add Stripe customer portal for Pro billing management #7443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,11 @@ enum ProUpgradePresenter { | |
| NSWorkspace.shared.open(AuthEnvironment.billingCheckoutURL) | ||
| } | ||
|
|
||
| @MainActor | ||
| static func presentBillingPortal() { | ||
| NSWorkspace.shared.open(AuthEnvironment.billingPortalURL) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Pro user is signed in only in the native app, this opens Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| @MainActor | ||
| private static func presentBrowserSplit(url: URL, transparentBackground: Bool) { | ||
| // Preferred: a browser split to the right of the focused pane, so the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,16 @@ | ||
| import { useTranslations } from "next-intl"; | ||
| import { getTranslations } from "next-intl/server"; | ||
| import { Suspense } from "react"; | ||
| import { SiteHeader } from "../components/site-header"; | ||
| import { ProCtaLink } from "../components/pro-cta-link"; | ||
| import { ProWelcomeBanner } from "../components/pro-welcome-banner"; | ||
| import { PRO_CHECKOUT_URL, TEAM_CHECKOUT_URL } from "../../lib/billing"; | ||
| import { DOWNLOAD_CONFIRMATION_HREF } from "../../lib/download"; | ||
| import { getStackServerApp, isStackConfigured } from "../../lib/stack"; | ||
| import { resolveProPlanStatus } from "../../../services/billing/pro"; | ||
| import { buildAlternates } from "../../../i18n/seo"; | ||
| import { | ||
| CurrentPlanBadge, | ||
| DisabledButton, | ||
| FeatureList, | ||
| PlanCard, | ||
| PricingCompareTable, | ||
|
|
@@ -27,6 +30,9 @@ import { | |
| // 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; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## pricing page\n'
sed -n '1,220p' web/app/'[locale]'/pricing/page.tsx | cat -n
printf '\n## billing pro service\n'
sed -n '1,260p' web/services/billing/pro.ts | cat -n
printf '\n## search for related symbols\n'
rg -n "currentPlanSnapshot|resolveProPlanStatus|syncProPlanMetadata|captureBillingError|getStackServerApp\(\)\.getUser" web -SRepository: manaflow-ai/cmux Length of output: 25473 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '## pricing page'
sed -n '1,220p' web/app/'[locale]'/pricing/page.tsx | cat -n
echo
echo '## billing pro service'
sed -n '1,260p' web/services/billing/pro.ts | cat -n
echo
echo '## symbol search'
rg -n "currentPlanSnapshot|resolveProPlanStatus|syncProPlanMetadata|captureBillingError|getStackServerApp\(\)\.getUser" web -SRepository: manaflow-ai/cmux Length of output: 25459 🏁 Script executed: #!/bin/bash
set -euo pipefail
for f in web/app/'[locale]'/pricing/page.tsx web/services/billing/pro.ts; do
echo "===== $f ====="
awk 'NR>=1 && NR<=260 { print NR ":" $0 }' "$f"
echo
doneRepository: manaflow-ai/cmux Length of output: 19002 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '260,310p' web/app/'[locale]'/pricing/page.tsx | cat -nRepository: manaflow-ai/cmux Length of output: 943 Guard 🤖 Prompt for AI Agents |
||
|
|
||
| export async function generateMetadata({ | ||
| params, | ||
|
|
@@ -42,8 +48,14 @@ export async function generateMetadata({ | |
| }; | ||
| } | ||
|
|
||
| export default function PricingPage() { | ||
| const t = useTranslations("pricing"); | ||
| export default async function PricingPage({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ locale: string }>; | ||
| }) { | ||
| const { locale } = await params; | ||
| const t = await getTranslations({ locale, namespace: "pricing" }); | ||
| const snapshot = await currentPlanSnapshot(); | ||
|
|
||
| const freeFeatures = t.raw("free.features") as string[]; | ||
| const proBaseFeatures = t.raw("pro.features") as string[]; | ||
|
|
@@ -96,10 +108,24 @@ export default function PricingPage() { | |
| name={t("pro.name")} | ||
| price={t("pro.price")} | ||
| period={t("perMonth")} | ||
| badge={ | ||
| snapshot.isPro ? ( | ||
| <CurrentPlanBadge>{t("currentPlan")}</CurrentPlanBadge> | ||
| ) : null | ||
| } | ||
| > | ||
| <ProCtaLink checkoutHref={PRO_CHECKOUT_URL} fallbackHref={DOWNLOAD_CONFIRMATION_HREF}> | ||
| {t("pro.cta")} | ||
| </ProCtaLink> | ||
| {snapshot.isPro ? ( | ||
| <div className="space-y-2"> | ||
| <DisabledButton>{t("currentPlan")}</DisabledButton> | ||
| <SecondaryLink href="/api/billing/portal"> | ||
| {t("manageBilling")} | ||
|
Comment on lines
+117
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Pro entitlement comes from the legacy Stack product path (the checkout route still supports Stack fallback, and Useful? React with 👍 / 👎. |
||
| </SecondaryLink> | ||
| </div> | ||
| ) : ( | ||
| <ProCtaLink checkoutHref={PRO_CHECKOUT_URL} fallbackHref={DOWNLOAD_CONFIRMATION_HREF}> | ||
| {t("pro.cta")} | ||
| </ProCtaLink> | ||
| )} | ||
| <p className="mt-5 text-sm font-medium">{t("pro.featuresLead")}</p> | ||
| <FeatureList items={proFeatures} /> | ||
| </PlanCard> | ||
|
|
@@ -156,14 +182,18 @@ export default function PricingPage() { | |
| </PrimaryLink> | ||
| ), | ||
| pro: ( | ||
| <ProCtaLink | ||
| checkoutHref={PRO_CHECKOUT_URL} | ||
| fallbackHref={DOWNLOAD_CONFIRMATION_HREF} | ||
| size="compact" | ||
| location="pricing_compare_header" | ||
| > | ||
| {t("pro.cta")} | ||
| </ProCtaLink> | ||
| snapshot.isPro ? ( | ||
| <DisabledButton size="compact">{t("currentPlan")}</DisabledButton> | ||
| ) : ( | ||
| <ProCtaLink | ||
| checkoutHref={PRO_CHECKOUT_URL} | ||
| fallbackHref={DOWNLOAD_CONFIRMATION_HREF} | ||
| size="compact" | ||
| location="pricing_compare_header" | ||
| > | ||
| {t("pro.cta")} | ||
| </ProCtaLink> | ||
| ) | ||
| ), | ||
| team: ( | ||
| <PrimaryLink href={TEAM_CHECKOUT_URL} size="compact"> | ||
|
|
@@ -239,3 +269,17 @@ export default function PricingPage() { | |
| </div> | ||
| ); | ||
| } | ||
|
|
||
| async function currentPlanSnapshot(): Promise<{ isPro: boolean }> { | ||
| if (!isStackConfigured()) { | ||
| return { isPro: false }; | ||
| } | ||
|
|
||
| const user = await getStackServerApp().getUser({ or: ANONYMOUS_IF_EXISTS }); | ||
| if (!user) { | ||
| return { isPro: false }; | ||
| } | ||
|
|
||
| const status = await resolveProPlanStatus(user); | ||
| return { isPro: status.isPro }; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Signed-in users miss Pro UIHigh Severity The new Reviewed by Cursor Bugbot for commit c738561. Configure here.
Comment on lines
+272
to
+285
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Duplicate Per the provided graph context, Extract this into a single shared helper (e.g. As per coding guidelines, "For correctness-critical decisions in this PR... ensure you use a single authoritative, structured source of truth rather than name/title heuristics or 'best effort' fallbacks... When you have multiple possible sources... make one the authority." 🤖 Prompt for AI AgentsSource: Path instructions |
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Consider adding unit coverage for
refreshBillingPlan()'s branches.This method decides whether the UI shows "Upgrade…" vs "Manage billing" and which action
openBillingPortal()/openProUpgrade()fires — a correctness-critical entitlement check — but there's no test exercising the no-user / non-2xx / decode-success / thrown-error paths (onlyAuthEnvironmentTestscovers URL resolution, not this fetch/decode logic).🤖 Prompt for AI Agents