Skip to content
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

Allow teams to sign up for Usage-Based Billing via Stripe #10378

Merged
merged 6 commits into from
Jun 8, 2022

Conversation

jankeromnes
Copy link
Contributor

@jankeromnes jankeromnes commented May 31, 2022

Description

Allow teams to sign up for Usage-Based Billing via Stripe.

Related Issue(s)

Fixes #10326

How to test

  1. Join the "Gitpod" team: invite link (this enables the Usage-Based feature flag for you)
  2. Create a new, different team ✨
  3. Navigate to your Team Settings > Team Billing
  4. Wait for the Usage-Based UI to appear above the Team Plans
  5. Upgrade your team to usage-based by providing a fake credit card (e.g. 4242 4242 4242 4242, 4/24, 424)
  6. This should successfully create a Stripe Customer for your team, visible in our Stripe Dashboard (credentials in 1Password)
No Team Billing Add Credit Card Active Team Billing Stripe Dashboard
Screenshot 2022-06-01 at 16 54 30 Screenshot 2022-06-01 at 16 55 02 Screenshot 2022-06-01 at 16 58 35 Screenshot 2022-06-01 at 16 56 42

Release Notes

NONE

Documentation

  • /werft with-payment=true

@roboquat roboquat added size/M and removed size/S labels May 31, 2022
@roboquat roboquat added size/L and removed size/M labels May 31, 2022
@jankeromnes jankeromnes force-pushed the jx/stripe-service branch 3 times, most recently from 1442173 to bb5c9c2 Compare June 1, 2022 10:08
@jankeromnes jankeromnes changed the title Allow users to sign up for Usage-Based Billing via Stripe Allow teams to sign up for Usage-Based Billing via Stripe Jun 1, 2022
@roboquat roboquat added size/XL and removed size/L labels Jun 1, 2022
@jankeromnes
Copy link
Contributor Author

jankeromnes commented Jun 1, 2022

TODO:

  • Implement a StripeService to manage Stripe customers
  • Implement server API for teams to sign up for usage-based billing via Stripe
  • Implement a basic Usage-Based Billing UI for Teams
  • Show a pending state when waiting for Stripe to create the customer
  • Show a Stripe portal when clicking on Manage → (follow-up?)

Known issue:

  • If you refresh the dashboard, the Usage-Based feature flag takes a while to be re-enabled (so, if you reload, the Usage-Based UI will be hidden for a few seconds, but otherwise still work well)

@jankeromnes jankeromnes marked this pull request as ready for review June 1, 2022 15:24
@jankeromnes jankeromnes requested a review from a team June 1, 2022 15:24
@github-actions github-actions bot added the team: webapp Issue belongs to the WebApp team label Jun 1, 2022
return this.config.stripeSettings?.publishableKey;
}

async getStripeSetupIntentClientSecret(ctx: TraceContext): Promise<string | undefined> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When interactions with 3rd party services fail, we shouldn't be propagating the errors directly to the client. We don't entirely control what is in the errors and may contain sensitive data. How is that handled here? I'd expect that we check for success/error and return appropriate obfuscated error messages upstream to the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree -- we could avoid propagating to the user/client with a try/catch:

// Pseudo-code
try {
    await stripeThing();
} catch (error) {
    log.error("the full error for our logs", { error });
    throw new Error("Stripe thing failed for some reason ¯\_(ツ)_/¯");
}

@easyCZ
Copy link
Member

easyCZ commented Jun 2, 2022

Behaviour wise: works well and tested.

The popup to add credit-card logs a number of errors when making a POST https://play.google.com/log?format=json&hasfast=true&authuser=0 net::ERR_BLOCKED_BY_CLIENT. Can this be removed? Why does it happen?

If you wanted to, this PR could've been split into the following:

  1. UI to show upgrade button which does nothing
  2. Server-side APIs (behind a feature flag) which return Unimplemented when part of team gitpod, NotAllowed otherwise.
  3. Server-side APIs actually implement behaviour
  4. UIs which hook up the behaviour.

That way, you'd be able to keep the PRs smaller, and individual steps smaller. For example, in this case you'd get the review feedback on needing the feature flag guard server-side on the API setup, not on the full PR.

@jankeromnes
Copy link
Contributor Author

jankeromnes commented Jun 7, 2022

@gtsiolis Had a quick look into how we can style Stripe's PaymentElement.

Here is how they suggest we do it:

  1. Start by picking a theme

The 4 themes are:

stripe night flat none
Screenshot 2022-06-07 at 15 10 36 Screenshot 2022-06-07 at 15 07 12 Screenshot 2022-06-07 at 15 11 00 Screenshot 2022-06-07 at 15 11 34

I think a good Minimum-Viable-Change could be:

  • Use stripe or flat in Light theme
  • Use night in Dark theme
  1. Customize the theme using variables

Set variables like fontFamily and colorPrimary to broadly customize components appearing throughout each Element.

We could do this to further align the look & feel of Stripe Elements with our dashboard in broad strokes. 💡

  1. If needed, fine-tune individual components and states using rules

With this, we can even drill down into specific styling issues that might remain after 1. and 2. 🔍

@jankeromnes jankeromnes requested a review from a team June 7, 2022 17:19
@jldec
Copy link
Contributor

jldec commented Jun 7, 2022

Works for me as well - Is there a way to navigate manually to the Stripe portal to manage billing/payment details (as a Gitpod user - not as the GitPod Stripe admin) ?

@jldec
Copy link
Contributor

jldec commented Jun 7, 2022

@easyCZ do you know why the feature flag feels slow ?

@jankeromnes
Copy link
Contributor Author

jankeromnes commented Jun 8, 2022

Works for me as well

Thanks! I'm glad this works perfectly fine for everyone. 😊

Is there a way to navigate manually to the Stripe portal to manage billing/payment details (as a Gitpod user - not as the GitPod Stripe admin) ?

I don't think so, but I'm planning to implement the Stripe portal in the next PR once this is merged.

@easyCZ
Copy link
Member

easyCZ commented Jun 8, 2022

@easyCZ do you know why the feature flag feels slow ?

To me it looks like react is not being told the values have changed so it doesn't propagate as soon as it should. I'm no expert on react either so not entirely sure where this delay lies.
The request for feature flags is made early and completes fast so it should be available at that point for rendering. I'll try to debug a bit when I have time but others more versed in React can probs debug this faster :)

@@ -1827,6 +1830,54 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
return subscription;
}

protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
const teams = await this.teamDB.findTeamsByUser(user.id);
const isUsageBasedBillingEnabled = await getExperimentsClient().getValueAsync(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work on the server? The implementation of getExperimentsClient() is

export function getExperimentsClient(): Client {
    // We have already instantiated a client, we can just re-use it.
    if (client !== undefined) {
        return client;
    }

    const host = window.location.hostname;
    if (host === "gitpod.io") {
        client = newProductionConfigCatClient();
    } else if (host === "gitpod-staging.com" || host.endsWith("gitpod-dev.com") || host.endsWith("gitpod-io-dev.com")) {
        client = newNonProductionConfigCatClient();
    } else {
        // We're gonna use a client which always returns the default value.
        client = newAlwaysReturningDefaultValueClient();
    }

    return client;
}

This uses the window.location.hostname which doesn't exist in the server context. How is this initialised?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If feature flags don't work as expected in the server, I'd vote for not using them right now (a client-side feature flag is enough to hide this WIP feature), and open a separate issue about fixing the server-side feature flags so that we can use them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jankeromnes How about adding a pre-pr to make it work on server properly and then using it here? I'm happy to pair if that makes it easier. Boy/girl scout rules apply.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@easyCZ That's the dashboard implementation, right?

The server-side implementation seems to be here: https://github.com/gitpod-io/gitpod/blob/main/components/server/src/experiments.ts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, wasn't aware this landed (and my IDE resolved the method to the dashboard one). This must be stray from another PR. It also lives under the root of server/src which it should not.

This will in fact work for server. However, there's a second problem with it as it hard-codes a single Client ID which is for the preview/staging environment, but not production.

Let's land as is, and fix it up in a follow-up PR. We'll need to move the file, but also inject the Client ID based on environment we're running in. The current version would also break self-hosted environments as it reaches out to an external source.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up issue: #10524

components/server/ee/src/workspace/gitpod-server-impl.ts Outdated Show resolved Hide resolved
@geropl
Copy link
Member

geropl commented Jun 8, 2022

If you refresh the dashboard, the Usage-Based feature flag takes a while to be re-enabled (so, if you reload, the Usage-Based UI will be hidden for a few seconds, but otherwise still work well)

This is not the case for me, works rather snappy now. 👍

Copy link
Member

@geropl geropl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, tested and works.

I leave it up to you @jankeromnes to address the "error leak" in a follow-up.

@jankeromnes
Copy link
Contributor Author

jankeromnes commented Jun 8, 2022

Awesome, many thanks @geropl. ❤️

Adding a brief hold for the try/catches:

/hold

@easyCZ
Copy link
Member

easyCZ commented Jun 8, 2022

@jankeromnes Let me know when API layer error handling is in and I'll re-review.

await this.guardTeamOperation(teamId, "update");
try {
const customer = await this.stripeService.findCustomerByTeamId(teamId);
return customer?.id || undefined;
Copy link
Member

@easyCZ easyCZ Jun 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, IMO you should return a 404 error when customer.id does not exist. Returning undefined prevents you classifying your errors and actually having reasonable metrics on what worked and didn't. This goes for setupIntent also.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, interesting point. I agree on the 404 semantics. However, returning undefined currently means that the customer doesn't exist yet, and is handled that way in the client (different from "the Stripe API failed"). If we switch this around to a 404, we'd need to update the client as well to handle 404 as a valid case (i.e. "the team is not yet a Stripe customer").

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's correct. If it doesn't exist, then it wasn't found and we should create one (if needed). In general, we should try to avoid OK responses with No data as much as possible, especially when we know why there is no data - in this case it doesn't exist in stripe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Follow-up PR/issue is fine)

@jankeromnes
Copy link
Contributor Author

jankeromnes commented Jun 8, 2022

/werft run with-clean-slate-deployment

👍 started the job as gitpod-build-jx-stripe-service.15
(with .werft/ from main)

@jankeromnes
Copy link
Contributor Author

jankeromnes commented Jun 8, 2022

Thanks again for the helpful reviews 🙏 and sorry for the bumpy ride. Will try even harder to keep PRs small going forward.

Build finally went through, and everything still works. 🎉

Pushing this over the finish line. 🏁 Many improvements to come as follow-ups. 🚀

/unhold

@roboquat roboquat merged commit 00c1085 into main Jun 8, 2022
@roboquat roboquat deleted the jx/stripe-service branch June 8, 2022 08:57
@roboquat roboquat added deployed: webapp Meta team change is running in production deployed Change is completely running in production labels Jun 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
deployed: webapp Meta team change is running in production deployed Change is completely running in production release-note-none size/XL team: webapp Issue belongs to the WebApp team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use Stripe Elements to capture CreditCard information then create a Stripe Customer and Subscription
6 participants