diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 4620cdeaa9803ca4c5daef1d95fb5090d70c0082..e96f752c98e64f0c0f5c2562061d564ee674f886 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 34adbd36c931232071e7489cc40526f10baeb77d..68f8fa5042e8fb491509ac59d6377868c6b48c10 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 f8b502cfa022e747521c4de30c97509e49650740..3511fb447ed730e8a635af27d35a9e6a38b53136 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 6d95aaa255bd0714230d99ae31f71918bdd7f7fd..f679987f8b0173b84eff7008393e7f351c01b7ad 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 db9c6d9eca2321baff28f67853314c47ee3b0fef..56ddc8d7ac76387b562af4a8bb6c94ccb062af1a 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 cb3b90dfe0b8494d83c20258324fd6f604df0e89..c13c98ad9aadd77ccf60c1f1cc3c33e6fac7690d 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.