Skip to content

Commit

Permalink
[server][dashboard] Allow new Stripe customers to select their billin…
Browse files Browse the repository at this point in the history
…g currency
  • Loading branch information
jankeromnes committed Aug 25, 2022
1 parent 803b52a commit 01bc480
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 26 deletions.
17 changes: 11 additions & 6 deletions components/dashboard/src/teams/TeamUsageBasedBilling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export default function TeamUsageBasedBilling() {
const { teams } = useContext(TeamsContext);
const location = useLocation();
const team = getCurrentTeam(location, teams);
const { currency } = useContext(PaymentContext);
const [teamBillingMode, setTeamBillingMode] = useState<BillingMode | undefined>(undefined);
const [stripeSubscriptionId, setStripeSubscriptionId] = useState<string | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(true);
Expand Down Expand Up @@ -87,7 +86,7 @@ export default function TeamUsageBasedBilling() {
JSON.stringify(pendingSubscription),
);
try {
await getGitpodService().server.subscribeTeamToStripe(team.id, setupIntentId, currency);
await getGitpodService().server.subscribeTeamToStripe(team.id, setupIntentId);
} catch (error) {
console.error("Could not subscribe team to Stripe", error);
window.localStorage.removeItem(`pendingStripeSubscriptionForTeam${team.id}`);
Expand Down Expand Up @@ -214,7 +213,9 @@ export default function TeamUsageBasedBilling() {
</div>
)}
</div>
{showBillingSetupModal && <BillingSetupModal onClose={() => setShowBillingSetupModal(false)} />}
{showBillingSetupModal && (
<BillingSetupModal teamId={team?.id || ""} onClose={() => setShowBillingSetupModal(false)} />
)}
{showUpdateLimitModal && (
<UpdateLimitModal
currentValue={spendingLimit}
Expand Down Expand Up @@ -281,7 +282,7 @@ function UpdateLimitModal(props: {
);
}

function BillingSetupModal(props: { onClose: () => void }) {
function BillingSetupModal(props: { teamId: string; onClose: () => void }) {
const { isDark } = useContext(ThemeContext);
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | undefined>();
const [stripeSetupIntentClientSecret, setStripeSetupIntentClientSecret] = useState<string | undefined>();
Expand All @@ -306,17 +307,18 @@ function BillingSetupModal(props: { onClose: () => void }) {
clientSecret: stripeSetupIntentClientSecret,
}}
>
<CreditCardInputForm />
<CreditCardInputForm teamId={props.teamId} />
</Elements>
)}
</div>
</Modal>
);
}

function CreditCardInputForm() {
function CreditCardInputForm(props: { teamId: string }) {
const stripe = useStripe();
const elements = useElements();
const { currency } = useContext(PaymentContext);
const [isLoading, setIsLoading] = useState<boolean>(false);

const handleSubmit = async (event: React.FormEvent) => {
Expand All @@ -326,6 +328,8 @@ function CreditCardInputForm() {
}
setIsLoading(true);
try {
// TODO(janx): Create Stripe customer for Team & currency
await getGitpodService().server.createOrUpdateStripeCustomerForTeam(props.teamId, currency);
const result = await stripe.confirmSetup({
elements,
confirmParams: {
Expand All @@ -350,6 +354,7 @@ function CreditCardInputForm() {

return (
<form className="mt-4 flex-grow flex flex-col" onSubmit={handleSubmit}>
<div>{/* FIXME(janx) */ currency}</div>
<PaymentElement />
<div className="mt-4 flex-grow flex flex-col justify-end items-end">
<button className="my-0 flex items-center space-x-2" disabled={!stripe || isLoading}>
Expand Down
4 changes: 2 additions & 2 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import {
import { RemotePageMessage, RemoteTrackMessage, RemoteIdentifyMessage } from "./analytics";
import { IDEServer } from "./ide-protocol";
import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol";
import { Currency } from "./plans";
import { BillableSession, BillableSessionRequest } from "./usage";
import { SupportedWorkspaceClass } from "./workspace-class";
import { BillingMode } from "./billing-mode";
Expand Down Expand Up @@ -290,7 +289,8 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getStripePublishableKey(): Promise<string>;
getStripeSetupIntentClientSecret(): Promise<string>;
findStripeSubscriptionIdForTeam(teamId: string): Promise<string | undefined>;
subscribeTeamToStripe(teamId: string, setupIntentId: string, currency: Currency): Promise<void>;
createOrUpdateStripeCustomerForTeam(teamId: string, currency: string): Promise<void>;
subscribeTeamToStripe(teamId: string, setupIntentId: string): Promise<void>;
getStripePortalUrlForTeam(teamId: string): Promise<string>;
getSpendingLimitForTeam(teamId: string): Promise<number | undefined>;
setSpendingLimitForTeam(teamId: string, spendingLimit: number): Promise<void>;
Expand Down
5 changes: 3 additions & 2 deletions components/server/ee/src/user/stripe-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,16 @@ export class StripeService {
await this.getStripe().subscriptions.del(subscriptionId);
}

async createSubscriptionForCustomer(customerId: string, currency: Currency): Promise<void> {
async createSubscriptionForCustomer(customer: Stripe.Customer): Promise<void> {
const currency = customer.currency || "USD";
const priceId = this.config?.stripeConfig?.usageProductPriceIds[currency];
if (!priceId) {
throw new Error(`No Stripe Price ID configured for currency '${currency}'`);
}
const startOfNextMonth = new Date(new Date().toISOString().slice(0, 7) + "-01"); // First day of this month (YYYY-MM-01)
startOfNextMonth.setMonth(startOfNextMonth.getMonth() + 1); // Add one month
await this.getStripe().subscriptions.create({
customer: customerId,
customer: customer.id,
items: [{ price: priceId }],
billing_cycle_anchor: Math.round(startOfNextMonth.getTime() / 1000),
});
Expand Down
29 changes: 21 additions & 8 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import {
TeamSubscriptionSlot,
TeamSubscriptionSlotResolved,
} from "@gitpod/gitpod-protocol/lib/team-subscription-protocol";
import { Currency, Plans } from "@gitpod/gitpod-protocol/lib/plans";
import { Plans } from "@gitpod/gitpod-protocol/lib/plans";
import * as pThrottle from "p-throttle";
import { formatDate } from "@gitpod/gitpod-protocol/lib/util/date-time";
import { FindUserByIdentityStrResult, UserService } from "../../../src/user/user-service";
Expand Down Expand Up @@ -2051,13 +2051,26 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
}

async createOrUpdateStripeCustomerForTeam(ctx: TraceContext, teamId: string, currency: string): Promise<void> {
this.checkAndBlockUser("createOrUpdateStripeCustomerForTeam");
const team = await this.guardTeamOperation(teamId, "update");
await this.ensureStripeApiIsAllowed({ team });
try {
let customer = await this.stripeService.findCustomerByTeamId(team!.id);
if (!customer) {
// customer = await this.stripeService.createCustomerForTeam(user, team!, setupIntentId);
}
} catch (error) {
log.error(`Failed to update Stripe customer profile for team '${teamId}'`, error);
throw new ResponseError(
ErrorCodes.INTERNAL_SERVER_ERROR,
`Failed to update Stripe customer profile for team '${teamId}'`,
);
}
}

protected defaultSpendingLimit = 100;
async subscribeTeamToStripe(
ctx: TraceContext,
teamId: string,
setupIntentId: string,
currency: Currency,
): Promise<void> {
async subscribeTeamToStripe(ctx: TraceContext, teamId: string, setupIntentId: string): Promise<void> {
const user = this.checkAndBlockUser("subscribeUserToStripe");
const team = await this.guardTeamOperation(teamId, "update");
await this.ensureStripeApiIsAllowed({ team });
Expand All @@ -2066,7 +2079,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
if (!customer) {
customer = await this.stripeService.createCustomerForTeam(user, team!, setupIntentId);
}
await this.stripeService.createSubscriptionForCustomer(customer.id, currency);
await this.stripeService.createSubscriptionForCustomer(customer);

const attributionId = AttributionId.render({ kind: "team", teamId });

Expand Down
1 change: 1 addition & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
getStripePublishableKey: { group: "default", points: 1 },
getStripeSetupIntentClientSecret: { group: "default", points: 1 },
findStripeSubscriptionIdForTeam: { group: "default", points: 1 },
createOrUpdateStripeCustomerForTeam: { group: "default", points: 1 },
subscribeTeamToStripe: { group: "default", points: 1 },
getStripePortalUrlForTeam: { group: "default", points: 1 },
listBilledUsage: { group: "default", points: 1 },
Expand Down
3 changes: 2 additions & 1 deletion components/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as yaml from "js-yaml";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import { filePathTelepresenceAware } from "@gitpod/gitpod-protocol/lib/env";
import { WorkspaceClasses, WorkspaceClassesConfig } from "./workspace/workspace-classes";
import { Currency } from "@gitpod/gitpod-protocol/lib/plans";

export const Config = Symbol("Config");
export type Config = Omit<
Expand All @@ -27,7 +28,7 @@ export type Config = Omit<
workspaceDefaults: WorkspaceDefaults;
chargebeeProviderOptions?: ChargebeeProviderOptions;
stripeSecrets?: { publishableKey: string; secretKey: string };
stripeConfig?: { usageProductPriceIds: { EUR: string; USD: string } };
stripeConfig?: { usageProductPriceIds: { [currency: string]: string } };
builtinAuthProvidersConfigured: boolean;
inactivityPeriodForRepos?: number;
};
Expand Down
11 changes: 4 additions & 7 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ import { Deferred } from "@gitpod/gitpod-protocol/lib/util/deferred";
import { InstallationAdminTelemetryDataProvider } from "../installation-admin/telemetry-data-provider";
import { LicenseEvaluator } from "@gitpod/licensor/lib";
import { Feature } from "@gitpod/licensor/lib/api";
import { Currency } from "@gitpod/gitpod-protocol/lib/plans";
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
import { BillableSession, BillableSessionRequest } from "@gitpod/gitpod-protocol/lib/usage";
import { WorkspaceClusterImagebuilderClientProvider } from "./workspace-cluster-imagebuilder-client-provider";
Expand Down Expand Up @@ -3181,12 +3180,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
async findStripeSubscriptionIdForTeam(ctx: TraceContext, teamId: string): Promise<string | undefined> {
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
}
async subscribeTeamToStripe(
ctx: TraceContext,
teamId: string,
setupIntentId: string,
currency: Currency,
): Promise<void> {
async createOrUpdateStripeCustomerForTeam(ctx: TraceContext, teamId: string, currency: string): Promise<void> {
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
}
async subscribeTeamToStripe(ctx: TraceContext, teamId: string, setupIntentId: string): Promise<void> {
throw new ResponseError(ErrorCodes.SAAS_FEATURE, `Not implemented in this version`);
}
async getStripePortalUrlForTeam(ctx: TraceContext, teamId: string): Promise<string> {
Expand Down

0 comments on commit 01bc480

Please sign in to comment.