diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 4620cdeaa9..e96f752c98 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -31,7 +31,7 @@ use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND}; use crate::rpc::{ResultExt as _, Server}; use crate::stripe_client::{ StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, - StripeSubscriptionId, + StripeSubscriptionId, UpdateCustomerParams, }; use crate::{AppState, Error, Result}; use crate::{db::UserId, llm::db::LlmDatabase}; @@ -353,7 +353,17 @@ async fn create_billing_subscription( } let customer_id = if let Some(existing_customer) = &existing_billing_customer { - StripeCustomerId(existing_customer.stripe_customer_id.clone().into()) + let customer_id = StripeCustomerId(existing_customer.stripe_customer_id.clone().into()); + if let Some(email) = user.email_address.as_deref() { + stripe_billing + .client() + .update_customer(&customer_id, UpdateCustomerParams { email: Some(email) }) + .await + // Update of email address is best-effort - continue checkout even if it fails + .context("error updating stripe customer email address") + .log_err(); + } + customer_id } else { stripe_billing .find_or_create_customer_by_email(user.email_address.as_deref()) diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 34adbd36c9..68f8fa5042 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -50,6 +50,10 @@ impl StripeBilling { } } + pub fn client(&self) -> &Arc { + &self.client + } + pub async fn initialize(&self) -> Result<()> { log::info!("StripeBilling: initializing"); diff --git a/crates/collab/src/stripe_client.rs b/crates/collab/src/stripe_client.rs index f8b502cfa0..3511fb447e 100644 --- a/crates/collab/src/stripe_client.rs +++ b/crates/collab/src/stripe_client.rs @@ -27,6 +27,11 @@ pub struct CreateCustomerParams<'a> { pub email: Option<&'a str>, } +#[derive(Debug)] +pub struct UpdateCustomerParams<'a> { + pub email: Option<&'a str>, +} + #[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)] pub struct StripeSubscriptionId(pub Arc); @@ -193,6 +198,12 @@ pub trait StripeClient: Send + Sync { async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result; + async fn update_customer( + &self, + customer_id: &StripeCustomerId, + params: UpdateCustomerParams<'_>, + ) -> Result; + async fn list_subscriptions_for_customer( &self, customer_id: &StripeCustomerId, diff --git a/crates/collab/src/stripe_client/fake_stripe_client.rs b/crates/collab/src/stripe_client/fake_stripe_client.rs index 6d95aaa255..f679987f8b 100644 --- a/crates/collab/src/stripe_client/fake_stripe_client.rs +++ b/crates/collab/src/stripe_client/fake_stripe_client.rs @@ -14,7 +14,7 @@ use crate::stripe_client::{ StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem, - StripeSubscriptionItemId, UpdateSubscriptionParams, + StripeSubscriptionItemId, UpdateCustomerParams, UpdateSubscriptionParams, }; #[derive(Debug, Clone)] @@ -95,6 +95,22 @@ impl StripeClient for FakeStripeClient { Ok(customer) } + async fn update_customer( + &self, + customer_id: &StripeCustomerId, + params: UpdateCustomerParams<'_>, + ) -> Result { + let mut customers = self.customers.lock(); + if let Some(customer) = customers.get_mut(customer_id) { + if let Some(email) = params.email { + customer.email = Some(email.to_string()); + } + Ok(customer.clone()) + } else { + Err(anyhow!("no customer found for {customer_id:?}")) + } + } + async fn list_subscriptions_for_customer( &self, customer_id: &StripeCustomerId, diff --git a/crates/collab/src/stripe_client/real_stripe_client.rs b/crates/collab/src/stripe_client/real_stripe_client.rs index db9c6d9eca..56ddc8d7ac 100644 --- a/crates/collab/src/stripe_client/real_stripe_client.rs +++ b/crates/collab/src/stripe_client/real_stripe_client.rs @@ -11,7 +11,7 @@ use stripe::{ CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, CreateCustomer, Customer, CustomerId, ListCustomers, Price, PriceId, Recurring, Subscription, - SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateSubscriptionItems, + SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateCustomer, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings, UpdateSubscriptionTrialSettingsEndBehavior, UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, }; @@ -25,7 +25,8 @@ use crate::stripe_client::{ StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionParams, + StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams, + UpdateSubscriptionParams, }; pub struct RealStripeClient { @@ -78,6 +79,24 @@ impl StripeClient for RealStripeClient { Ok(StripeCustomer::from(customer)) } + async fn update_customer( + &self, + customer_id: &StripeCustomerId, + params: UpdateCustomerParams<'_>, + ) -> Result { + let customer = Customer::update( + &self.client, + &customer_id.try_into()?, + UpdateCustomer { + email: params.email, + ..Default::default() + }, + ) + .await?; + + Ok(StripeCustomer::from(customer)) + } + async fn list_subscriptions_for_customer( &self, customer_id: &StripeCustomerId, diff --git a/docs/src/accounts.md b/docs/src/accounts.md index cb3b90dfe0..c13c98ad9a 100644 --- a/docs/src/accounts.md +++ b/docs/src/accounts.md @@ -29,4 +29,4 @@ To sign out of Zed, you can use either of these methods: Your Zed account's email address is the address provided by GitHub OAuth. If you have a public email address then it will be used, otherwise your primary GitHub email address will be used. Changes to your email address on GitHub can be synced to your Zed account by [signing in to zed.dev](https://zed.dev/sign_in). -Stripe is used for billing, and will use your Zed account's email address at the time of first use of Zed AI. Changes to your Zed account email address do not currently update the email address used in Stripe. See [Updating Billing Information](./ai/billing.md#updating-billing-info) for how to change this email address. +Stripe is used for billing, and will use your Zed account's email address when starting a subscription. Changes to your Zed account email address do not currently update the email address used in Stripe. See [Updating Billing Information](./ai/billing.md#updating-billing-info) for how to change this email address.