diff --git a/components/dashboard/src/settings/Billing.tsx b/components/dashboard/src/settings/Billing.tsx index 778fecb060194c..fd49f75adfc49a 100644 --- a/components/dashboard/src/settings/Billing.tsx +++ b/components/dashboard/src/settings/Billing.tsx @@ -4,14 +4,48 @@ * See License-AGPL.txt in the project root for license information. */ -import { useContext } from "react"; +import { Team } from "@gitpod/gitpod-protocol"; +import { useContext, useEffect, useState } from "react"; import { Link } from "react-router-dom"; +import getSettingsMenu from "./settings-menu"; +import { ReactComponent as Spinner } from "../icons/Spinner.svg"; +import DropDown from "../components/DropDown"; import { PageWithSubMenu } from "../components/PageWithSubMenu"; import { PaymentContext } from "../payment-context"; -import getSettingsMenu from "./settings-menu"; +import { getGitpodService } from "../service/service"; +import { TeamsContext } from "../teams/teams-context"; +import { UserContext } from "../user-context"; export default function Billing() { + const { user } = useContext(UserContext); const { showPaymentUI, showUsageBasedUI } = useContext(PaymentContext); + const { teams } = useContext(TeamsContext); + const [teamsWithBillingEnabled, setTeamsWithBillingEnabled] = useState(); + + useEffect(() => { + if (!teams) { + setTeamsWithBillingEnabled(undefined); + return; + } + const teamsWithBilling: Team[] = []; + Promise.all( + teams.map(async (t) => { + const subscriptionId = await getGitpodService().server.findStripeSubscriptionIdForTeam(t.id); + if (subscriptionId) { + teamsWithBilling.push(t); + } + }), + ).then(() => setTeamsWithBillingEnabled(teamsWithBilling)); + }, [teams]); + + const setUsageAttributionTeam = async (team?: Team) => { + if (!user) { + return; + } + const additionalData = user.additionalData || {}; + additionalData.usageAttributionId = team ? `team:${team.id}` : `user:${user.id}`; + await getGitpodService().server.updateLoggedInUser({ additionalData }); + }; return (

Usage-Based Billing

Manage usage-based billing, spending limit, and payment method.

-

- Hint:{" "} - - Create a team - {" "} - to set up usage-based billing. -

+
+

Billing Account

+ {teamsWithBillingEnabled === undefined && } + {teamsWithBillingEnabled && teamsWithBillingEnabled.length === 0 && ( +
+ + + Create a team + {" "} + to set up usage-based billing. + +
+ )} + {teamsWithBillingEnabled && teamsWithBillingEnabled.length > 0 && ( +
+ Bill all my usage to: + `team:${t.id}` === user?.additionalData?.usageAttributionId, + )?.name + } + customClasses="w-32" + renderAsLink={true} + entries={[ + { + title: "(myself)", + onClick: () => setUsageAttributionTeam(undefined), + }, + ].concat( + teamsWithBillingEnabled.map((t) => ({ + title: t.name, + onClick: () => setUsageAttributionTeam(t), + })), + )} + /> +
+ )} +
); } diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 01c6dd0d6a3da5..df6ca44934c4d7 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -150,9 +150,10 @@ export interface AdditionalUserData { oauthClientsApproved?: { [key: string]: string }; // to remember GH Orgs the user installed/updated the GH App for knownGitHubOrgs?: string[]; - // Git clone URL pointing to the user's dotfile repo dotfileRepo?: string; + // Identifies an explicit team or user ID to which all the user's workspace usage should be attributed to (e.g. for billing purposes) + usageAttributionId?: string; } export interface EmailNotificationSettings { diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index ee8f5ea73aa81f..7c15762c1270c4 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -210,10 +210,29 @@ export class UserService { * Identifies the team or user to which a workspace instance's running time should be attributed to * (e.g. for usage analytics or billing purposes). * + * A. Billing-based attribution: If payments are enabled, we attribute all the user's usage to: + * - An explicitly selected billing account (e.g. a team with usage-based billing enabled) + * - Or, we default to the user for all usage (e.g. free tier or individual billing, regardless of project/team) + * + * B. Project-based attribution: If payments are not enabled (e.g. Self-Hosted), we attribute: + * - To the owner of the project (user or team), if the workspace is linked to a project + * - To the user, iff the workspace is not linked to a project + * * @param user * @param projectId */ async getWorkspaceUsageAttributionId(user: User, projectId?: string): Promise { + // A. Billing-based attribution + if (this.config.enablePayment) { + if (!user.additionalData?.usageAttributionId) { + // No explicit user attribution ID yet -- attribute all usage to the user by default (regardless of project/team). + return `user:${user.id}`; + } + // Return the user's explicit attribution ID. + return user.additionalData.usageAttributionId; + } + + // B. Project-based attribution if (!projectId) { // No project -- attribute to the user. return `user:${user.id}`;