From ab3228d13d935cdca8adff615a7459978445191d Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 13:14:41 -0700 Subject: [PATCH 1/4] chore: remove all Stripe billing code and database schema Billing was an enterprise entitlement that is no longer needed. This removes the Stripe integration, billing UI, subscription management, and related database fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.development | 6 - .../migration.sql | 15 ++ packages/db/prisma/schema.prisma | 9 - packages/shared/src/entitlements.ts | 1 - packages/shared/src/env.server.ts | 6 - packages/web/package.json | 4 - packages/web/src/__mocks__/prisma.ts | 3 - packages/web/src/actions.ts | 33 +-- .../components/navigationMenu/index.tsx | 5 - .../navigationMenu/trialIndicator.tsx | 42 --- .../app/[domain]/components/upgradeGuard.tsx | 31 --- packages/web/src/app/[domain]/layout.tsx | 20 -- .../app/[domain]/settings/billing/page.tsx | 112 -------- .../web/src/app/[domain]/settings/layout.tsx | 7 - .../members/components/inviteMemberCard.tsx | 5 +- .../app/[domain]/settings/members/page.tsx | 2 - .../web/src/app/[domain]/upgrade/page.tsx | 82 ------ .../web/src/app/api/(server)/stripe/route.ts | 105 -------- .../web/src/ee/features/billing/actions.ts | 248 ------------------ .../components/changeBillingEmailCard.tsx | 109 -------- .../features/billing/components/checkout.tsx | 90 ------- .../components/enterpriseUpgradeCard.tsx | 29 -- .../components/manageSubscriptionButton.tsx | 49 ---- .../billing/components/teamUpgradeCard.tsx | 59 ----- .../billing/components/upgradeCard.tsx | 55 ---- .../src/ee/features/billing/serverUtils.ts | 80 ------ .../web/src/ee/features/billing/stripe.ts | 11 - .../src/features/userManagement/actions.ts | 17 -- packages/web/src/lib/authUtils.ts | 9 - packages/web/src/lib/constants.ts | 15 -- packages/web/src/lib/errorCodes.ts | 4 - packages/web/src/lib/serviceError.ts | 15 -- 32 files changed, 23 insertions(+), 1255 deletions(-) create mode 100644 packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql delete mode 100644 packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx delete mode 100644 packages/web/src/app/[domain]/components/upgradeGuard.tsx delete mode 100644 packages/web/src/app/[domain]/settings/billing/page.tsx delete mode 100644 packages/web/src/app/[domain]/upgrade/page.tsx delete mode 100644 packages/web/src/app/api/(server)/stripe/route.ts delete mode 100644 packages/web/src/ee/features/billing/actions.ts delete mode 100644 packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx delete mode 100644 packages/web/src/ee/features/billing/components/checkout.tsx delete mode 100644 packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx delete mode 100644 packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx delete mode 100644 packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx delete mode 100644 packages/web/src/ee/features/billing/components/upgradeCard.tsx delete mode 100644 packages/web/src/ee/features/billing/serverUtils.ts delete mode 100644 packages/web/src/ee/features/billing/stripe.ts diff --git a/.env.development b/.env.development index 84670f852..0eca799ff 100644 --- a/.env.development +++ b/.env.development @@ -40,12 +40,6 @@ CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exist # Redis REDIS_URL="redis://localhost:6379" -# Stripe -# STRIPE_SECRET_KEY: z.string().optional(), -# STRIPE_PRODUCT_ID: z.string().optional(), -# STRIPE_WEBHOOK_SECRET: z.string().optional(), -# STRIPE_ENABLE_TEST_CLOCKS=false - # Agents # GITHUB_APP_ID= diff --git a/packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql b/packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql new file mode 100644 index 000000000..436c17849 --- /dev/null +++ b/packages/db/prisma/migrations/20260401200619_remove_stripe_billing/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - You are about to drop the column `stripeCustomerId` on the `Org` table. All the data in the column will be lost. + - You are about to drop the column `stripeLastUpdatedAt` on the `Org` table. All the data in the column will be lost. + - You are about to drop the column `stripeSubscriptionStatus` on the `Org` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Org" DROP COLUMN "stripeCustomerId", +DROP COLUMN "stripeLastUpdatedAt", +DROP COLUMN "stripeSubscriptionStatus"; + +-- DropEnum +DROP TYPE "StripeSubscriptionStatus"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 609c71b89..0c58714b6 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -19,11 +19,6 @@ enum ConnectionSyncStatus { FAILED } -enum StripeSubscriptionStatus { - ACTIVE - INACTIVE -} - enum ChatVisibility { PRIVATE PUBLIC @@ -283,10 +278,6 @@ model Org { memberApprovalRequired Boolean @default(true) - stripeCustomerId String? - stripeSubscriptionStatus StripeSubscriptionStatus? - stripeLastUpdatedAt DateTime? - /// List of pending invites to this organization invites Invite[] diff --git a/packages/shared/src/entitlements.ts b/packages/shared/src/entitlements.ts index 0effb0a58..de841a0dd 100644 --- a/packages/shared/src/entitlements.ts +++ b/packages/shared/src/entitlements.ts @@ -30,7 +30,6 @@ export type Plan = keyof typeof planLabels; // eslint-disable-next-line @typescript-eslint/no-unused-vars const entitlements = [ "search-contexts", - "billing", "anonymous-access", "multi-tenancy", "sso", diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 95006d875..3b852cac1 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -163,12 +163,6 @@ const options = { SMTP_PASSWORD: z.string().optional(), EMAIL_FROM_ADDRESS: z.string().email().optional(), - // Stripe - STRIPE_SECRET_KEY: z.string().optional(), - STRIPE_PRODUCT_ID: z.string().optional(), - STRIPE_WEBHOOK_SECRET: z.string().optional(), - STRIPE_ENABLE_TEST_CLOCKS: booleanSchema.default('false'), - LOGTAIL_TOKEN: z.string().optional(), LOGTAIL_HOST: z.string().url().optional(), diff --git a/packages/web/package.json b/packages/web/package.json index 35eefe539..ceee7d2a8 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -11,7 +11,6 @@ "openapi:generate": "tsx tools/generateOpenApi.ts", "generate:protos": "proto-loader-gen-types --includeComments --longs=Number --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --keepCase --includeDirs=../../vendor/zoekt/grpc/protos --outDir=src/proto zoekt/webserver/v1/webserver.proto zoekt/webserver/v1/query.proto", "dev:emails": "email dev --dir ./src/emails", - "stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe", "tool:decrypt-jwe": "tsx tools/decryptJWE.ts" }, "dependencies": { @@ -102,8 +101,6 @@ "@sourcebot/schemas": "workspace:*", "@sourcebot/shared": "workspace:*", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "@stripe/react-stripe-js": "^3.1.1", - "@stripe/stripe-js": "^5.6.0", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.53.3", "@tanstack/react-table": "^8.20.5", @@ -187,7 +184,6 @@ "slate-history": "^0.113.1", "slate-react": "^0.117.1", "strip-json-comments": "^5.0.1", - "stripe": "^17.6.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "use-stick-to-bottom": "^1.1.3", diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts index 8a0d9544c..acf687294 100644 --- a/packages/web/src/__mocks__/prisma.ts +++ b/packages/web/src/__mocks__/prisma.ts @@ -19,9 +19,6 @@ export const MOCK_ORG: Org = { imageUrl: null, metadata: null, memberApprovalRequired: false, - stripeCustomerId: null, - stripeSubscriptionStatus: null, - stripeLastUpdatedAt: null, inviteLinkEnabled: false, inviteLinkId: null } diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 60ffe332e..f3581cbed 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -10,7 +10,7 @@ import { prisma } from "@/prisma"; import { render } from "@react-email/components"; import * as Sentry from '@sentry/nextjs'; import { generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/shared"; -import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db"; +import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db"; import { createLogger } from "@sourcebot/shared"; import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type"; import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; @@ -22,8 +22,6 @@ import { createTransport } from "nodemailer"; import { Octokit } from "octokit"; import { auth } from "./auth"; import { getOrgFromDomain } from "./data/org"; -import { getSubscriptionForOrg } from "./ee/features/billing/serverUtils"; -import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe"; import InviteUserEmail from "./emails/inviteUserEmail"; import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail"; import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail"; @@ -188,31 +186,12 @@ export const withTenancyModeEnforcement = async(mode: TenancyMode, fn: () => export const completeOnboarding = async (domain: string): Promise<{ success: boolean } | ServiceError> => sew(() => withAuth((userId) => withOrgMembership(userId, domain, async ({ org }) => { - // If billing is not enabled, we can just mark the org as onboarded. - if (!IS_BILLING_ENABLED) { - await prisma.org.update({ - where: { id: org.id }, - data: { - isOnboarded: true, - } - }); - - // Else, validate that the org has an active subscription. - } else { - const subscriptionOrError = await getSubscriptionForOrg(org.id, prisma); - if (isServiceError(subscriptionOrError)) { - return subscriptionOrError; + await prisma.org.update({ + where: { id: org.id }, + data: { + isOnboarded: true, } - - await prisma.org.update({ - where: { id: org.id }, - data: { - isOnboarded: true, - stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE, - stripeLastUpdatedAt: new Date(), - } - }); - } + }); return { success: true, diff --git a/packages/web/src/app/[domain]/components/navigationMenu/index.tsx b/packages/web/src/app/[domain]/components/navigationMenu/index.tsx index 8c70f5699..79b671539 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu/index.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/index.tsx @@ -4,8 +4,6 @@ import { auth } from "@/auth"; import { Button } from "@/components/ui/button"; import { NavigationMenu as NavigationMenuBase } from "@/components/ui/navigation-menu"; import { Separator } from "@/components/ui/separator"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { env } from "@sourcebot/shared"; import { ServiceErrorException } from "@/lib/serviceError"; import { isServiceError } from "@/lib/utils"; @@ -16,7 +14,6 @@ import { MeControlDropdownMenu } from "../meControlDropdownMenu"; import WhatsNewIndicator from "../whatsNewIndicator"; import { NavigationItems } from "./navigationItems"; import { ProgressIndicator } from "./progressIndicator"; -import { TrialIndicator } from "./trialIndicator"; import { redirect } from "next/navigation"; import { AppearanceDropdownMenu } from "../appearanceDropdownMenu"; @@ -28,7 +25,6 @@ interface NavigationMenuProps { export const NavigationMenu = async ({ domain, }: NavigationMenuProps) => { - const subscription = IS_BILLING_ENABLED ? await getSubscriptionInfo(domain) : null; const session = await auth(); const isAuthenticated = session?.user !== undefined; @@ -134,7 +130,6 @@ export const NavigationMenu = async ({ numberOfReposWithFirstTimeIndexingJobsInProgress={numberOfReposWithFirstTimeIndexingJobsInProgress} sampleRepos={sampleRepos} /> - {session ? ( { - const domain = useDomain(); - const captureEvent = useCaptureEvent(); - - if (isServiceError(subscription)) { - captureEvent('wa_trial_nav_subscription_fetch_fail', { - errorCode: subscription.errorCode, - }); - return null; - } - - if (!subscription || subscription.status !== "trialing") { - return null; - } - - return ( - captureEvent('wa_trial_nav_pressed', {})}> -
- - - {/* eslint-disable-next-line react-hooks/purity -- Date.now() during render is intentional for displaying remaining trial days */} - {Math.ceil((subscription.nextBillingDate * 1000 - Date.now()) / (1000 * 60 * 60 * 24))} days left in trial - -
- - ); -}; diff --git a/packages/web/src/app/[domain]/components/upgradeGuard.tsx b/packages/web/src/app/[domain]/components/upgradeGuard.tsx deleted file mode 100644 index f948eafa7..000000000 --- a/packages/web/src/app/[domain]/components/upgradeGuard.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; - -import { Redirect } from "@/app/components/redirect"; -import { useDomain } from "@/hooks/useDomain"; -import { usePathname } from "next/navigation"; -import { useMemo } from "react"; - -interface UpgradeGuardProps { - children: React.ReactNode; -} - -export const UpgradeGuard = ({ children }: UpgradeGuardProps) => { - const domain = useDomain(); - const pathname = usePathname(); - - const content = useMemo(() => { - if (!pathname.endsWith('/upgrade')) { - return ( - - ) - } else { - return children; - } - }, [domain, children, pathname]); - - return content; -} - - diff --git a/packages/web/src/app/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx index 3a1f48b05..3a4371cae 100644 --- a/packages/web/src/app/[domain]/layout.tsx +++ b/packages/web/src/app/[domain]/layout.tsx @@ -3,16 +3,13 @@ import { auth } from "@/auth"; import { getOrgFromDomain } from "@/data/org"; import { isServiceError } from "@/lib/utils"; import { OnboardGuard } from "./components/onboardGuard"; -import { UpgradeGuard } from "./components/upgradeGuard"; import { cookies, headers } from "next/headers"; import { getSelectorsByUserAgent } from "react-device-detect"; import { MobileUnsupportedSplashScreen } from "./components/mobileUnsupportedSplashScreen"; import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME } from "@/lib/constants"; import { SyntaxReferenceGuide } from "./components/syntaxReferenceGuide"; import { SyntaxGuideProvider } from "./components/syntaxGuideProvider"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { notFound, redirect } from "next/navigation"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; import { PendingApprovalCard } from "./components/pendingApproval"; import { SubmitJoinRequest } from "./components/submitJoinRequest"; import { hasEntitlement } from "@sourcebot/shared"; @@ -152,23 +149,6 @@ export default async function Layout(props: LayoutProps) { } } - if (IS_BILLING_ENABLED) { - const subscription = await getSubscriptionInfo(domain); - if ( - subscription && - ( - isServiceError(subscription) || - (subscription.status !== "active" && subscription.status !== "trialing") - ) - ) { - return ( - - {children} - - ) - } - } - const headersList = await headers(); const cookieStore = await cookies() const userAgent = headersList.get('user-agent'); diff --git a/packages/web/src/app/[domain]/settings/billing/page.tsx b/packages/web/src/app/[domain]/settings/billing/page.tsx deleted file mode 100644 index 6c5ce8d0a..000000000 --- a/packages/web/src/app/[domain]/settings/billing/page.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { getCurrentUserRole } from "@/actions" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { getSubscriptionBillingEmail, getSubscriptionInfo } from "@/ee/features/billing/actions" -import { ChangeBillingEmailCard } from "@/ee/features/billing/components/changeBillingEmailCard" -import { ManageSubscriptionButton } from "@/ee/features/billing/components/manageSubscriptionButton" -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe" -import { ServiceErrorException } from "@/lib/serviceError" -import { isServiceError } from "@/lib/utils" -import { CalendarIcon, DollarSign, Users } from "lucide-react" -import type { Metadata } from "next" -import { notFound } from "next/navigation" - -export const metadata: Metadata = { - title: "Billing | Settings", - description: "Manage your subscription and billing information", -} - -interface BillingPageProps { - params: Promise<{ - domain: string - }> -} - -export default async function BillingPage(props: BillingPageProps) { - const params = await props.params; - - const { - domain - } = params; - - if (!IS_BILLING_ENABLED) { - notFound(); - } - - const subscription = await getSubscriptionInfo(domain) - - if (isServiceError(subscription)) { - throw new ServiceErrorException(subscription); - } - - if (!subscription) { - throw new Error("Subscription not found"); - } - - const currentUserRole = await getCurrentUserRole(domain) - if (isServiceError(currentUserRole)) { - throw new ServiceErrorException(currentUserRole); - } - - const billingEmail = await getSubscriptionBillingEmail(domain); - if (isServiceError(billingEmail)) { - throw new ServiceErrorException(billingEmail); - } - - return ( -
-
-

Billing

-

Manage your subscription and billing information

-
-
- {/* Billing Email Card */} - - - - - Subscription Plan - - - {subscription.status === "trialing" - ? "You are currently on a free trial" - : `You are currently on the ${subscription.plan} plan.`} - - - -
-
- -
-

Seats

-

{subscription.seats} active users

-
-
-
-
-
- -
-

{subscription.status === "trialing" ? "Trial End Date" : "Next Billing Date"}

-

{new Date(subscription.nextBillingDate * 1000).toLocaleDateString()}

-
-
-
-
-
- -
-

Billing Amount

-

${(subscription.perSeatPrice * subscription.seats).toFixed(2)} per month

-
-
-
-
- - - -
- -
-
- ) -} diff --git a/packages/web/src/app/[domain]/settings/layout.tsx b/packages/web/src/app/[domain]/settings/layout.tsx index 0eb17b83e..857cef673 100644 --- a/packages/web/src/app/[domain]/settings/layout.tsx +++ b/packages/web/src/app/[domain]/settings/layout.tsx @@ -2,7 +2,6 @@ import React from "react" import { Metadata } from "next" import { SidebarNav } from "./components/sidebar-nav" import { NavigationMenu } from "../components/navigationMenu" -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { isServiceError } from "@/lib/utils"; @@ -84,12 +83,6 @@ export const getSidebarNavItems = async () => } return [ - ...(IS_BILLING_ENABLED ? [ - { - title: "Billing", - href: `/${SINGLE_TENANT_ORG_DOMAIN}/settings/billing`, - } - ] : []), ...(role === OrgRole.OWNER ? [ { title: "Access", diff --git a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx index d7a8ab736..da248dd94 100644 --- a/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/inviteMemberCard.tsx @@ -29,11 +29,10 @@ export const inviteMemberFormSchema = z.object({ interface InviteMemberCardProps { currentUserRole: OrgRole; - isBillingEnabled: boolean; seatsAvailable?: boolean; } -export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvailable = true }: InviteMemberCardProps) => { +export const InviteMemberCard = ({ currentUserRole, seatsAvailable = true }: InviteMemberCardProps) => { const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const domain = useDomain(); @@ -164,7 +163,7 @@ export const InviteMemberCard = ({ currentUserRole, isBillingEnabled, seatsAvail Invite Team Members - {`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization. ${isBillingEnabled ? "Your subscription's seat count will be adjusted when a member accepts their invitation." : ""}`} + {`Your team is growing! By confirming, you will be inviting ${form.getValues().emails.length} new members to your organization.`}
diff --git a/packages/web/src/app/[domain]/settings/members/page.tsx b/packages/web/src/app/[domain]/settings/members/page.tsx index be2c1e8f0..8a16e5726 100644 --- a/packages/web/src/app/[domain]/settings/members/page.tsx +++ b/packages/web/src/app/[domain]/settings/members/page.tsx @@ -7,7 +7,6 @@ import { Tabs, TabsContent } from "@/components/ui/tabs"; import { TabSwitcher } from "@/components/ui/tab-switcher"; import { InvitesList } from "./components/invitesList"; import { getOrgInvites, getMe, getOrgAccountRequests } from "@/actions"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { ServiceErrorException } from "@/lib/serviceError"; import { getSeats, hasEntitlement, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared"; import { RequestsList } from "./components/requestsList"; @@ -99,7 +98,6 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp diff --git a/packages/web/src/app/[domain]/upgrade/page.tsx b/packages/web/src/app/[domain]/upgrade/page.tsx deleted file mode 100644 index 3d4e0e306..000000000 --- a/packages/web/src/app/[domain]/upgrade/page.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { Footer } from "@/app/components/footer"; -import { OrgSelector } from "../components/orgSelector"; -import { EnterpriseUpgradeCard } from "@/ee/features/billing/components/enterpriseUpgradeCard"; -import { TeamUpgradeCard } from "@/ee/features/billing/components/teamUpgradeCard"; -import { redirect } from "next/navigation"; -import { isServiceError } from "@/lib/utils"; -import Link from "next/link"; -import { ArrowLeftIcon } from "@radix-ui/react-icons"; -import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; -import { env } from "@sourcebot/shared"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; - -export default async function Upgrade(props: { params: Promise<{ domain: string }> }) { - const params = await props.params; - - const { - domain - } = params; - - if (!IS_BILLING_ENABLED) { - redirect(`/${domain}`); - } - - const subscription = await getSubscriptionInfo(domain); - if (!subscription) { - redirect(`/${domain}`); - } - - if (!isServiceError(subscription) && subscription.status === "active") { - redirect(`/${domain}`); - } - - const isTrialing = !isServiceError(subscription) ? subscription.status === "trialing" : false; - - return ( -
- {isTrialing && ( - -
- Return to dashboard -
- - )} - -
- -

- {isTrialing ? - "Upgrade your trial." : - "Your subscription has expired." - } -

-

- {isTrialing ? - "Upgrade now to get the most out of Sourcebot." : - "Please upgrade to continue using Sourcebot." - } -

-
- - {env.SOURCEBOT_TENANCY_MODE === 'multi' && ( - - )} - -
- - -
- -
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/api/(server)/stripe/route.ts b/packages/web/src/app/api/(server)/stripe/route.ts deleted file mode 100644 index 212860437..000000000 --- a/packages/web/src/app/api/(server)/stripe/route.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { headers } from 'next/headers'; -import { NextRequest } from 'next/server'; -import Stripe from 'stripe'; -import { prisma } from '@/prisma'; -import { StripeSubscriptionStatus } from '@sourcebot/db'; -import { stripeClient } from '@/ee/features/billing/stripe'; -import { env } from '@sourcebot/shared'; -import { createLogger } from "@sourcebot/shared"; - -const logger = createLogger('stripe-webhook'); - -export async function POST(req: NextRequest) { - const body = await req.text(); - const signature = (await headers()).get('stripe-signature'); - - if (!signature) { - return new Response('No signature', { status: 400 }); - } - - if (!stripeClient) { - return new Response('Stripe client not initialized', { status: 500 }); - } - - if (!env.STRIPE_WEBHOOK_SECRET) { - return new Response('Stripe webhook secret not set', { status: 500 }); - } - - try { - const event = stripeClient.webhooks.constructEvent( - body, - signature, - env.STRIPE_WEBHOOK_SECRET - ); - - if (event.type === 'customer.subscription.deleted') { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - - const org = await prisma.org.findFirst({ - where: { - stripeCustomerId: customerId - } - }); - - if (!org) { - return new Response('Org not found', { status: 404 }); - } - - await prisma.org.update({ - where: { - id: org.id - }, - data: { - stripeSubscriptionStatus: StripeSubscriptionStatus.INACTIVE, - stripeLastUpdatedAt: new Date() - } - }); - logger.info(`Org ${org.id} subscription status updated to INACTIVE`); - - return new Response(JSON.stringify({ received: true }), { - status: 200 - }); - } else if (event.type === 'customer.subscription.created') { - const subscription = event.data.object as Stripe.Subscription; - const customerId = subscription.customer as string; - - const org = await prisma.org.findFirst({ - where: { - stripeCustomerId: customerId - } - }); - - if (!org) { - return new Response('Org not found', { status: 404 }); - } - - await prisma.org.update({ - where: { - id: org.id - }, - data: { - stripeSubscriptionStatus: StripeSubscriptionStatus.ACTIVE, - stripeLastUpdatedAt: new Date() - } - }); - logger.info(`Org ${org.id} subscription status updated to ACTIVE`); - - return new Response(JSON.stringify({ received: true }), { - status: 200 - }); - } else { - logger.info(`Received unknown event type: ${event.type}`); - return new Response(JSON.stringify({ received: true }), { - status: 202 - }); - } - - } catch (err) { - logger.error('Error processing webhook:', err); - return new Response( - 'Webhook error: ' + (err as Error).message, - { status: 400 } - ); - } -} diff --git a/packages/web/src/ee/features/billing/actions.ts b/packages/web/src/ee/features/billing/actions.ts deleted file mode 100644 index d66e94eec..000000000 --- a/packages/web/src/ee/features/billing/actions.ts +++ /dev/null @@ -1,248 +0,0 @@ -'use server'; - -import { getMe, sew, withAuth } from "@/actions"; -import { ServiceError, stripeClientNotInitialized, notFound } from "@/lib/serviceError"; -import { withOrgMembership } from "@/actions"; -import { prisma } from "@/prisma"; -import { OrgRole } from "@sourcebot/db"; -import { stripeClient } from "./stripe"; -import { isServiceError } from "@/lib/utils"; -import { env } from "@sourcebot/shared"; -import { StatusCodes } from "http-status-codes"; -import { ErrorCode } from "@/lib/errorCodes"; -import { headers } from "next/headers"; -import { getSubscriptionForOrg } from "./serverUtils"; -import { createLogger } from "@sourcebot/shared"; - -const logger = createLogger('billing-actions'); - -export const createOnboardingSubscription = async (domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const user = await getMe(); - if (isServiceError(user)) { - return user; - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const test_clock = env.STRIPE_ENABLE_TEST_CLOCKS === 'true' ? await stripeClient.testHelpers.testClocks.create({ - frozen_time: Math.floor(Date.now() / 1000) - }) : null; - - // Use the existing customer if it exists, otherwise create a new one. - const customerId = await (async () => { - if (org.stripeCustomerId) { - return org.stripeCustomerId; - } - - const customer = await stripeClient.customers.create({ - name: org.name, - email: user.email ?? undefined, - test_clock: test_clock?.id, - description: `Created by ${user.email} on ${domain} (id: ${org.id})`, - }); - - await prisma.org.update({ - where: { - id: org.id, - }, - data: { - stripeCustomerId: customer.id, - } - }); - - return customer.id; - })(); - - const existingSubscription = await getSubscriptionForOrg(org.id, prisma); - if (!isServiceError(existingSubscription)) { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.SUBSCRIPTION_ALREADY_EXISTS, - message: "Attempted to create a trial subscription for an organization that already has an active subscription", - } satisfies ServiceError; - } - - - const prices = await stripeClient.prices.list({ - product: env.STRIPE_PRODUCT_ID, - expand: ['data.product'], - }); - - try { - const subscription = await stripeClient.subscriptions.create({ - customer: customerId, - items: [{ - price: prices.data[0].id, - }], - trial_period_days: 14, - trial_settings: { - end_behavior: { - missing_payment_method: 'cancel', - }, - }, - payment_settings: { - save_default_payment_method: 'on_subscription', - }, - }); - - if (!subscription) { - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR, - message: "Failed to create subscription", - } satisfies ServiceError; - } - - return { - subscriptionId: subscription.id, - } - } catch (e) { - logger.error(e); - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR, - message: "Failed to create subscription", - } satisfies ServiceError; - } - }, /* minRequiredRole = */ OrgRole.OWNER) - )); - -export const createStripeCheckoutSession = async (domain: string) => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const orgMembers = await prisma.userToOrg.findMany({ - where: { - orgId: org.id, - }, - select: { - userId: true, - } - }); - const numOrgMembers = orgMembers.length; - - const origin = (await headers()).get('origin')!; - const prices = await stripeClient.prices.list({ - product: env.STRIPE_PRODUCT_ID, - expand: ['data.product'], - }); - - const stripeSession = await stripeClient.checkout.sessions.create({ - customer: org.stripeCustomerId as string, - payment_method_types: ['card'], - line_items: [ - { - price: prices.data[0].id, - quantity: numOrgMembers - } - ], - mode: 'subscription', - payment_method_collection: 'always', - success_url: `${origin}/${domain}/settings/billing`, - cancel_url: `${origin}/${domain}`, - }); - - if (!stripeSession.url) { - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CHECKOUT_ERROR, - message: "Failed to create checkout session", - } satisfies ServiceError; - } - - return { - url: stripeSession.url, - } - }) - )); - -export const getCustomerPortalSessionLink = async (domain: string): Promise => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const origin = (await headers()).get('origin')!; - const portalSession = await stripeClient.billingPortal.sessions.create({ - customer: org.stripeCustomerId as string, - return_url: `${origin}/${domain}/settings/billing`, - }); - - return portalSession.url; - }, /* minRequiredRole = */ OrgRole.OWNER) - )); - -export const getSubscriptionBillingEmail = async (domain: string): Promise => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const customer = await stripeClient.customers.retrieve(org.stripeCustomerId); - if (!('email' in customer) || customer.deleted) { - return notFound(); - } - return customer.email!; - }) - )); - -export const changeSubscriptionBillingEmail = async (domain: string, newEmail: string): Promise<{ success: boolean } | ServiceError> => sew(() => - withAuth((userId) => - withOrgMembership(userId, domain, async ({ org }) => { - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - await stripeClient.customers.update(org.stripeCustomerId, { - email: newEmail, - }); - - return { - success: true, - } - }, /* minRequiredRole = */ OrgRole.OWNER) - )); - -export const getSubscriptionInfo = async (domain: string) => sew(() => - withAuth(async (userId) => - withOrgMembership(userId, domain, async ({ org }) => { - const subscription = await getSubscriptionForOrg(org.id, prisma); - - if (isServiceError(subscription)) { - return subscription; - } - - return { - status: subscription.status, - plan: "Team", - seats: subscription.items.data[0].quantity!, - perSeatPrice: subscription.items.data[0].price.unit_amount! / 100, - nextBillingDate: subscription.current_period_end!, - } - }) - )); diff --git a/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx b/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx deleted file mode 100644 index 674b8fac8..000000000 --- a/packages/web/src/ee/features/billing/components/changeBillingEmailCard.tsx +++ /dev/null @@ -1,109 +0,0 @@ -"use client" - -import { useToast } from "@/components/hooks/use-toast" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { changeSubscriptionBillingEmail } from "@/ee/features/billing/actions" -import useCaptureEvent from "@/hooks/useCaptureEvent" -import { useDomain } from "@/hooks/useDomain" -import { isServiceError } from "@/lib/utils" -import { zodResolver } from "@hookform/resolvers/zod" -import { OrgRole } from "@sourcebot/db" -import { Loader2 } from "lucide-react" -import { useRouter } from "next/navigation" -import { useState } from "react" -import { useForm } from "react-hook-form" -import * as z from "zod" - -const formSchema = z.object({ - email: z.string().email("Please enter a valid email address"), -}) - -interface ChangeBillingEmailCardProps { - currentUserRole: OrgRole, - billingEmail: string -} - -export function ChangeBillingEmailCard({ currentUserRole, billingEmail }: ChangeBillingEmailCardProps) { - const domain = useDomain() - const [isLoading, setIsLoading] = useState(false) - const { toast } = useToast() - const captureEvent = useCaptureEvent(); - const router = useRouter() - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: billingEmail, - }, - }) - - const onSubmit = async (values: z.infer) => { - setIsLoading(true) - const newEmail = values.email || billingEmail - const result = await changeSubscriptionBillingEmail(domain, newEmail) - if (!isServiceError(result)) { - toast({ - description: "✅ Billing email updated successfully!", - }) - captureEvent('wa_billing_email_updated_success', {}) - router.refresh() - } else { - toast({ - description: "❌ Failed to update billing email. Please try again.", - }) - captureEvent('wa_billing_email_updated_fail', { - errorCode: result.errorCode, - }) - } - setIsLoading(false) - } - - return ( - - - - Billing Email - - The email address for your billing account - - -
- - ( - - - - - - - )} - /> -
- -
- - -
-
- ) -} - diff --git a/packages/web/src/ee/features/billing/components/checkout.tsx b/packages/web/src/ee/features/billing/components/checkout.tsx deleted file mode 100644 index 41dc7dc41..000000000 --- a/packages/web/src/ee/features/billing/components/checkout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { useToast } from "@/components/hooks/use-toast"; -import { Button } from "@/components/ui/button"; -import { useDomain } from "@/hooks/useDomain"; -import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; -import { ErrorCode } from "@/lib/errorCodes"; -import { isServiceError } from "@/lib/utils"; -import { Check, Loader2 } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import { OnboardingSteps, TEAM_FEATURES } from "@/lib/constants"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { createOnboardingSubscription } from "../actions"; - -export const Checkout = () => { - const domain = useDomain(); - const { toast } = useToast(); - const errorCode = useNonEmptyQueryParam('errorCode'); - const errorMessage = useNonEmptyQueryParam('errorMessage'); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - const captureEvent = useCaptureEvent(); - - useEffect(() => { - if (errorCode === ErrorCode.STRIPE_CHECKOUT_ERROR && errorMessage) { - toast({ - description: `⚠️ Stripe checkout failed with error: ${errorMessage}`, - variant: "destructive", - }); - captureEvent('wa_onboard_checkout_fail', { - errorCode: errorMessage, - }); - } - }, [errorCode, errorMessage, toast, captureEvent]); - - const onCheckout = useCallback(() => { - setIsLoading(true); - createOnboardingSubscription(domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Stripe checkout failed with error: ${response.message}`, - variant: "destructive", - }) - captureEvent('wa_onboard_checkout_fail', { - errorCode: response.errorCode, - }); - } else { - captureEvent('wa_onboard_checkout_success', {}); - router.push(`/${domain}/onboard?step=${OnboardingSteps.Complete}`); - } - }) - .finally(() => { - setIsLoading(false); - }); - }, [domain, router, toast, captureEvent]); - - return ( -
- -

Start your 14 day free trial

-

Cancel anytime. No credit card required.

-
    - {TEAM_FEATURES.map((feature, index) => ( -
  • -
    - -
    -

    {feature}

    -
  • - ))} -
-
- -
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx b/packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx deleted file mode 100644 index 74de5cf08..000000000 --- a/packages/web/src/ee/features/billing/components/enterpriseUpgradeCard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { ENTERPRISE_FEATURES, SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants"; -import { UpgradeCard } from "./upgradeCard"; -import Link from "next/link"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; - - -export const EnterpriseUpgradeCard = () => { - const captureEvent = useCaptureEvent(); - - const onClick = () => { - captureEvent('wa_enterprise_upgrade_card_pressed', {}); - } - - return ( - - - - ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx b/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx deleted file mode 100644 index 25a04a86a..000000000 --- a/packages/web/src/ee/features/billing/components/manageSubscriptionButton.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client" - -import { useState } from "react" -import { useRouter } from "next/navigation" -import { isServiceError } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { useDomain } from "@/hooks/useDomain"; -import { OrgRole } from "@sourcebot/db"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { ExternalLink, Loader2 } from "lucide-react"; -import { getCustomerPortalSessionLink } from "@/ee/features/billing/actions" - -export function ManageSubscriptionButton({ currentUserRole }: { currentUserRole: OrgRole }) { - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - const domain = useDomain(); - const captureEvent = useCaptureEvent(); - - const redirectToCustomerPortal = async () => { - setIsLoading(true) - const session = await getCustomerPortalSessionLink(domain); - if (isServiceError(session)) { - captureEvent('wa_manage_subscription_button_create_portal_session_fail', { - errorCode: session.errorCode, - }); - setIsLoading(false); - } else { - captureEvent('wa_manage_subscription_button_create_portal_session_success', {}) - router.push(session) - // @note: we don't want to set isLoading to false here since we want to show the loading - // spinner until the page is redirected. - } - } - - const isOwner = currentUserRole === OrgRole.OWNER - return ( -
- -
- ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx b/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx deleted file mode 100644 index 26ff5276f..000000000 --- a/packages/web/src/ee/features/billing/components/teamUpgradeCard.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; - -import { UpgradeCard } from "./upgradeCard"; -import { useToast } from "@/components/hooks/use-toast"; -import { useDomain } from "@/hooks/useDomain"; -import { isServiceError } from "@/lib/utils"; -import { useCallback, useState } from "react"; -import { useRouter } from "next/navigation"; -import { TEAM_FEATURES } from "@/lib/constants"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { createStripeCheckoutSession } from "../actions"; - -interface TeamUpgradeCardProps { - buttonText: string; -} - -export const TeamUpgradeCard = ({ buttonText }: TeamUpgradeCardProps) => { - const domain = useDomain(); - const { toast } = useToast(); - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const captureEvent = useCaptureEvent(); - - const onClick = useCallback(() => { - captureEvent('wa_team_upgrade_card_pressed', {}); - setIsLoading(true); - createStripeCheckoutSession(domain) - .then((response) => { - if (isServiceError(response)) { - toast({ - description: `❌ Stripe checkout failed with error: ${response.message}`, - variant: "destructive", - }); - captureEvent('wa_team_upgrade_checkout_fail', { - errorCode: response.errorCode, - }); - } else { - router.push(response.url); - captureEvent('wa_team_upgrade_checkout_success', {}); - } - }) - .finally(() => { - setIsLoading(false); - }); - }, [domain, router, toast, captureEvent]); - - return ( - - ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/components/upgradeCard.tsx b/packages/web/src/ee/features/billing/components/upgradeCard.tsx deleted file mode 100644 index 9d24b254b..000000000 --- a/packages/web/src/ee/features/billing/components/upgradeCard.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Check, Loader2 } from "lucide-react"; - - -interface UpgradeCardProps { - title: string; - description: string; - price: string; - priceDescription: string; - features: string[]; - buttonText: string; - onClick?: () => void; - isLoading?: boolean; -} - -export const UpgradeCard = ({ title, description, price, priceDescription, features, buttonText, onClick, isLoading = false }: UpgradeCardProps) => { - return ( - onClick?.()} - > - - {title} - {description} - - -
-

{price}

-

{priceDescription}

-
-
    - {features.map((feature, index) => ( -
  • - - {feature} -
  • - ))} -
-
- - - -
- ) -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/serverUtils.ts b/packages/web/src/ee/features/billing/serverUtils.ts deleted file mode 100644 index bf2eb6a8c..000000000 --- a/packages/web/src/ee/features/billing/serverUtils.ts +++ /dev/null @@ -1,80 +0,0 @@ -import 'server-only'; - -import { notFound, orgInvalidSubscription, ServiceError, stripeClientNotInitialized } from "@/lib/serviceError"; -import { isServiceError } from "@/lib/utils"; -import { Prisma } from "@sourcebot/db"; -import Stripe from "stripe"; -import { stripeClient } from "./stripe"; - -export const incrementOrgSeatCount = async (orgId: number, prisma: Prisma.TransactionClient) => { - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const subscription = await getSubscriptionForOrg(orgId, prisma); - if (isServiceError(subscription)) { - return subscription; - } - - const existingSeatCount = subscription.items.data[0].quantity; - const newSeatCount = (existingSeatCount || 1) + 1; - - await stripeClient.subscriptionItems.update( - subscription.items.data[0].id, - { - quantity: newSeatCount, - proration_behavior: 'create_prorations', - } - ); -} - -export const decrementOrgSeatCount = async (orgId: number, prisma: Prisma.TransactionClient) => { - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const subscription = await getSubscriptionForOrg(orgId, prisma); - if (isServiceError(subscription)) { - return subscription; - } - - const existingSeatCount = subscription.items.data[0].quantity; - const newSeatCount = (existingSeatCount || 1) - 1; - - await stripeClient.subscriptionItems.update( - subscription.items.data[0].id, - { - quantity: newSeatCount, - proration_behavior: 'create_prorations', - } - ); -} - -export const getSubscriptionForOrg = async (orgId: number, prisma: Prisma.TransactionClient): Promise => { - const org = await prisma.org.findUnique({ - where: { - id: orgId, - }, - }); - - if (!org) { - return notFound(); - } - - if (!org.stripeCustomerId) { - return notFound(); - } - - if (!stripeClient) { - return stripeClientNotInitialized(); - } - - const subscriptions = await stripeClient.subscriptions.list({ - customer: org.stripeCustomerId - }); - - if (subscriptions.data.length === 0) { - return orgInvalidSubscription(); - } - return subscriptions.data[0]; -} \ No newline at end of file diff --git a/packages/web/src/ee/features/billing/stripe.ts b/packages/web/src/ee/features/billing/stripe.ts deleted file mode 100644 index c8ca0af7a..000000000 --- a/packages/web/src/ee/features/billing/stripe.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'server-only'; -import { env } from '@sourcebot/shared' -import Stripe from "stripe"; -import { hasEntitlement } from '@sourcebot/shared'; - -export const IS_BILLING_ENABLED = hasEntitlement('billing') && env.STRIPE_SECRET_KEY !== undefined; - -export const stripeClient = - IS_BILLING_ENABLED - ? new Stripe(env.STRIPE_SECRET_KEY!) - : undefined; \ No newline at end of file diff --git a/packages/web/src/features/userManagement/actions.ts b/packages/web/src/features/userManagement/actions.ts index 02869c659..298ebfa69 100644 --- a/packages/web/src/features/userManagement/actions.ts +++ b/packages/web/src/features/userManagement/actions.ts @@ -3,12 +3,9 @@ import { sew } from "@/actions"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, ServiceError } from "@/lib/serviceError"; -import { isServiceError } from "@/lib/utils"; import { prisma } from "@/prisma"; import { withAuthV2, withMinimumOrgRole } from "@/withAuthV2"; import { OrgRole, Prisma } from "@sourcebot/db"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { decrementOrgSeatCount } from "@/ee/features/billing/serverUtils"; import { StatusCodes } from "http-status-codes"; export const removeMemberFromOrg = async (memberId: string): Promise<{ success: boolean } | ServiceError> => sew(() => @@ -54,13 +51,6 @@ export const removeMemberFromOrg = async (memberId: string): Promise<{ success: } }); - if (IS_BILLING_ENABLED) { - const result = await decrementOrgSeatCount(org.id, tx); - if (isServiceError(result)) { - throw result; - } - } - return null; }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }); @@ -101,13 +91,6 @@ export const leaveOrg = async (): Promise<{ success: boolean } | ServiceError> = } }); - if (IS_BILLING_ENABLED) { - const result = await decrementOrgSeatCount(org.id, tx); - if (isServiceError(result)) { - throw result; - } - } - return null; }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable }); diff --git a/packages/web/src/lib/authUtils.ts b/packages/web/src/lib/authUtils.ts index f250ad233..6326be64b 100644 --- a/packages/web/src/lib/authUtils.ts +++ b/packages/web/src/lib/authUtils.ts @@ -9,8 +9,6 @@ import { createLogger } from "@sourcebot/shared"; import { getAuditService } from "@/ee/features/audit/factory"; import { StatusCodes } from "http-status-codes"; import { ErrorCode } from "./errorCodes"; -import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; -import { incrementOrgSeatCount } from "@/ee/features/billing/serverUtils"; import { getOrgFromDomain } from "@/data/org"; const logger = createLogger('web-auth-utils'); @@ -262,13 +260,6 @@ export const addUserToOrganization = async (userId: string, orgId: number): Prom } }); - if (IS_BILLING_ENABLED) { - const result = await incrementOrgSeatCount(orgId, tx); - if (isServiceError(result)) { - throw result; - } - } - // Delete the account request if it exists since we've added the user to the org const accountRequest = await tx.accountRequest.findUnique({ where: { diff --git a/packages/web/src/lib/constants.ts b/packages/web/src/lib/constants.ts index 21bb97d54..d6dbaf5e9 100644 --- a/packages/web/src/lib/constants.ts +++ b/packages/web/src/lib/constants.ts @@ -4,24 +4,9 @@ export enum OnboardingSteps { CreateOrg = 'create-org', ConnectCodeHost = 'connect-code-host', InviteTeam = 'invite-team', - Checkout = 'checkout', Complete = 'complete', } -export const ENTERPRISE_FEATURES = [ - "All Team features", - "Dedicated Slack support channel", - "Single tenant deployment", - "Advanced security features", -] - -export const TEAM_FEATURES = [ - "Index thousands of repos from multiple code hosts (GitHub, GitLab, Gerrit, Gitea, etc.). Self-hosted code hosts supported.", - "Public and private repos supported.", - "Create shareable links to code snippets.", - "Built on-top of zoekt, Google's code search engine. Blazingly fast and powerful (regex, symbol) code search.", -] - export const MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME = 'sb.mobile-unsupported-splash-screen-dismissed'; export const AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME = 'sb.agentic-search-tutorial-dismissed'; export const OPTIONAL_PROVIDERS_LINK_SKIPPED_COOKIE_NAME = 'sb.optional-providers-link-skipped'; diff --git a/packages/web/src/lib/errorCodes.ts b/packages/web/src/lib/errorCodes.ts index a3e897eac..0f589f28b 100644 --- a/packages/web/src/lib/errorCodes.ts +++ b/packages/web/src/lib/errorCodes.ts @@ -12,7 +12,6 @@ export enum ErrorCode { ORG_NOT_FOUND = 'ORG_NOT_FOUND', CONNECTION_SYNC_ALREADY_SCHEDULED = 'CONNECTION_SYNC_ALREADY_SCHEDULED', ORG_DOMAIN_ALREADY_EXISTS = 'ORG_DOMAIN_ALREADY_EXISTS', - ORG_INVALID_SUBSCRIPTION = 'ORG_INVALID_SUBSCRIPTION', INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', CONNECTION_NOT_FAILED = 'CONNECTION_NOT_FAILED', @@ -20,10 +19,7 @@ export enum ErrorCode { INVALID_INVITE = 'INVALID_INVITE', INVALID_INVITE_LINK = 'INVALID_INVITE_LINK', INVITE_LINK_NOT_ENABLED = 'INVITE_LINK_NOT_ENABLED', - STRIPE_CHECKOUT_ERROR = 'STRIPE_CHECKOUT_ERROR', SECRET_ALREADY_EXISTS = 'SECRET_ALREADY_EXISTS', - SUBSCRIPTION_ALREADY_EXISTS = 'SUBSCRIPTION_ALREADY_EXISTS', - STRIPE_CLIENT_NOT_INITIALIZED = 'STRIPE_CLIENT_NOT_INITIALIZED', ACTION_DISALLOWED_IN_TENANCY_MODE = 'ACTION_DISALLOWED_IN_TENANCY_MODE', SEARCH_CONTEXT_NOT_FOUND = 'SEARCH_CONTEXT_NOT_FOUND', MISSING_ORG_DOMAIN_HEADER = 'MISSING_ORG_DOMAIN_HEADER', diff --git a/packages/web/src/lib/serviceError.ts b/packages/web/src/lib/serviceError.ts index cb4af0227..a34a15805 100644 --- a/packages/web/src/lib/serviceError.ts +++ b/packages/web/src/lib/serviceError.ts @@ -128,14 +128,6 @@ export const orgDomainExists = (): ServiceError => { } } -export const orgInvalidSubscription = (): ServiceError => { - return { - statusCode: StatusCodes.BAD_REQUEST, - errorCode: ErrorCode.ORG_INVALID_SUBSCRIPTION, - message: "Invalid subscription", - } -} - export const secretAlreadyExists = (): ServiceError => { return { statusCode: StatusCodes.CONFLICT, @@ -152,10 +144,3 @@ export const invalidGitRef = (ref: string): ServiceError => { }; } -export const stripeClientNotInitialized = (): ServiceError => { - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorCode: ErrorCode.STRIPE_CLIENT_NOT_INITIALIZED, - message: "Stripe client is not initialized.", - } -} \ No newline at end of file From da00676b76eb7a7e4cdc885f3ab386e6c976a099 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 13:15:09 -0700 Subject: [PATCH 2/4] docs: add CHANGELOG entry for billing removal Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f2db5fa..d973c4502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed +- [EE] Removed Stripe billing integration, including subscription management, billing UI, and related database schema [#1069](https://github.com/sourcebot-dev/sourcebot/pull/1069) + ## [4.16.4] - 2026-04-01 ### Added From 2aadfdf23229d630451a1dc034eb5bd29f6271e7 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 13:18:54 -0700 Subject: [PATCH 3/4] update lock file --- yarn.lock | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/yarn.lock b/yarn.lock index b2cd9e5d5..659ac594d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8974,8 +8974,6 @@ __metadata: "@sourcebot/schemas": "workspace:*" "@sourcebot/shared": "workspace:*" "@ssddanbrown/codemirror-lang-twig": "npm:^1.0.0" - "@stripe/react-stripe-js": "npm:^3.1.1" - "@stripe/stripe-js": "npm:^5.6.0" "@tailwindcss/typography": "npm:^0.5.16" "@tanstack/eslint-plugin-query": "npm:^5.74.7" "@tanstack/react-query": "npm:^5.53.3" @@ -9083,7 +9081,6 @@ __metadata: slate-history: "npm:^0.113.1" slate-react: "npm:^0.117.1" strip-json-comments: "npm:^5.0.1" - stripe: "npm:^17.6.0" tailwind-merge: "npm:^2.5.2" tailwindcss: "npm:^3.4.1" tailwindcss-animate: "npm:^1.0.7" @@ -9119,26 +9116,6 @@ __metadata: languageName: node linkType: hard -"@stripe/react-stripe-js@npm:^3.1.1": - version: 3.5.1 - resolution: "@stripe/react-stripe-js@npm:3.5.1" - dependencies: - prop-types: "npm:^15.7.2" - peerDependencies: - "@stripe/stripe-js": ">=1.44.1 <7.0.0" - react: ">=16.8.0 <20.0.0" - react-dom: ">=16.8.0 <20.0.0" - checksum: 10c0/999f52c420657a9a4f287de12f4e1c560e168f10f8e12f6c6f4314fb170478d4c594fc8dcec7f3bdc4413f21cbc23155f8849b19ed0589e5bcb558f065c63cfe - languageName: node - linkType: hard - -"@stripe/stripe-js@npm:^5.6.0": - version: 5.10.0 - resolution: "@stripe/stripe-js@npm:5.10.0" - checksum: 10c0/0309007baaf939931de0e18ec8230bdfa41b1644cd5f65d72168ef0a05ea4d14da9a95ff628c94f9f1e222577296369b7490534803ce1ce18d08a557ae562644 - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.15": version: 0.5.15 resolution: "@swc/helpers@npm:0.5.15" @@ -9603,7 +9580,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=8.1.0, @types/node@npm:^22.7.5": +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.7.5": version: 22.13.11 resolution: "@types/node@npm:22.13.11" dependencies: @@ -19011,7 +18988,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -21242,16 +21219,6 @@ __metadata: languageName: node linkType: hard -"stripe@npm:^17.6.0": - version: 17.7.0 - resolution: "stripe@npm:17.7.0" - dependencies: - "@types/node": "npm:>=8.1.0" - qs: "npm:^6.11.0" - checksum: 10c0/df67c6d455bd0dd87140640924c220fa9581fc00c3267d171f407c8d088f946f61e3ae7e88a89e7dd705b10fd5254630fc943222eb6f003390ebafbd391f81b2 - languageName: node - linkType: hard - "strnum@npm:^2.1.2": version: 2.1.2 resolution: "strnum@npm:2.1.2" From cc455bfcc0d3cdafb88a095a0505ae66a3792e31 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 13:22:08 -0700 Subject: [PATCH 4/4] remove changelog --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d973c4502..d2f2db5fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Removed -- [EE] Removed Stripe billing integration, including subscription management, billing UI, and related database schema [#1069](https://github.com/sourcebot-dev/sourcebot/pull/1069) - ## [4.16.4] - 2026-04-01 ### Added