diff --git a/components/dashboard/src/teams/TeamUsageBasedBilling.tsx b/components/dashboard/src/teams/TeamUsageBasedBilling.tsx index 36e937ea382e11..7042ed59e835af 100644 --- a/components/dashboard/src/teams/TeamUsageBasedBilling.tsx +++ b/components/dashboard/src/teams/TeamUsageBasedBilling.tsx @@ -27,6 +27,7 @@ export default function TeamUsageBasedBilling() { const [showBillingSetupModal, setShowBillingSetupModal] = useState(false); const [pendingStripeCustomer, setPendingStripeCustomer] = useState(); const [pollStripeCustomerTimeout, setPollStripeCustomerTimeout] = useState(); + const [stripePortalUrl, setStripePortalUrl] = useState(); useEffect(() => { if (!team) { @@ -46,6 +47,16 @@ export default function TeamUsageBasedBilling() { })(); }, [team]); + useEffect(() => { + if (!team || !stripeCustomerId) { + return; + } + (async () => { + const portalUrl = await getGitpodService().server.getStripePortalUrlForTeam(team.id); + setStripePortalUrl(portalUrl); + })(); + }, [team, stripeCustomerId]); + useEffect(() => { if (!team) { return; @@ -144,9 +155,11 @@ export default function TeamUsageBasedBilling() {
Active
- {/* */} + + + )} @@ -156,6 +169,12 @@ export default function TeamUsageBasedBilling() { ); } +function getStripeAppearance(isDark?: boolean): Appearance { + return { + theme: isDark ? "night" : "stripe", + }; +} + function BillingSetupModal(props: { onClose: () => void }) { const { isDark } = useContext(ThemeContext); const [stripePromise, setStripePromise] = useState | undefined>(); @@ -169,12 +188,6 @@ function BillingSetupModal(props: { onClose: () => void }) { ]).then((setters) => setters.forEach((s) => s())); }, []); - const getStripeAppearance = (): Appearance => { - return { - theme: isDark ? "night" : "stripe", - }; - }; - return (

Upgrade Billing

@@ -182,7 +195,10 @@ function BillingSetupModal(props: { onClose: () => void }) { {!!stripePromise && !!stripeSetupIntentClientSecret && ( diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 63e474dd24d7a7..702000319e6063 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -274,6 +274,7 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, getStripeSetupIntentClientSecret(): Promise; findStripeCustomerIdForTeam(teamId: string): Promise; subscribeTeamToStripe(teamId: string, setupIntentId: string): Promise; + getStripePortalUrlForTeam(teamId: string): Promise; /** * Analytics diff --git a/components/server/ee/src/user/stripe-service.ts b/components/server/ee/src/user/stripe-service.ts index 3f3700c25cc947..d172ae15736901 100644 --- a/components/server/ee/src/user/stripe-service.ts +++ b/components/server/ee/src/user/stripe-service.ts @@ -101,4 +101,16 @@ export class StripeService { }); return customer; } + + async getPortalUrlForTeam(team: Team): Promise { + const customer = await this.findCustomerByTeamId(team.id); + if (!customer) { + throw new Error(`No Stripe Customer ID found for team '${team.id}'`); + } + const session = await this.getStripe().billingPortal.sessions.create({ + customer: customer.id, + return_url: this.config.hostUrl.with(() => ({ pathname: `/t/${team.slug}/billing` })).toString(), + }); + return session.url; + } } diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 4704709d9f6938..905328e977d1d9 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -1906,6 +1906,23 @@ export class GitpodServerEEImpl extends GitpodServerImpl { } } + async getStripePortalUrlForTeam(ctx: TraceContext, teamId: string): Promise { + const user = this.checkAndBlockUser("getStripePortalUrlForTeam"); + await this.ensureIsUsageBasedFeatureFlagEnabled(user); + await this.guardTeamOperation(teamId, "update"); + const team = await this.teamDB.findTeamById(teamId); + try { + const url = await this.stripeService.getPortalUrlForTeam(team!); + return url; + } catch (error) { + log.error(`Failed to get Stripe portal URL for team '${teamId}'`, error); + throw new ResponseError( + ErrorCodes.INTERNAL_SERVER_ERROR, + `Failed to get Stripe portal URL for team '${teamId}'`, + ); + } + } + // (SaaS) – admin async adminGetAccountStatement(ctx: TraceContext, userId: string): Promise { traceAPIParams(ctx, { userId }); diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 8c53ba61763647..d1c5acd145df31 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -200,6 +200,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig { getStripeSetupIntentClientSecret: { group: "default", points: 1 }, findStripeCustomerIdForTeam: { group: "default", points: 1 }, subscribeTeamToStripe: { group: "default", points: 1 }, + getStripePortalUrlForTeam: { group: "default", points: 1 }, trackEvent: { group: "default", points: 1 }, trackLocation: { group: "default", points: 1 }, identifyUser: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 09cfddef3fa258..f91725bc1c1b03 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -3045,6 +3045,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { async subscribeTeamToStripe(ctx: TraceContext, teamId: string, setupIntentId: string): Promise { throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); } + async getStripePortalUrlForTeam(ctx: TraceContext, teamId: string): Promise { + throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`); + } // //#endregion }