From ca72efe7010439a2b4c082e8fc554a486d97aae7 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 23 May 2025 22:02:02 +0200 Subject: [PATCH] Add overdue invoices check (#31290) - Rename current_user_account_too_young to account_too_young for consistency - Add has_overdue_invoices field to track billing status - Block edit predictions when user has overdue invoices - Add overdue invoice warning to inline completion menu Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- crates/agent/src/agent_panel.rs | 2 +- crates/client/src/user.rs | 12 +++++- crates/collab/src/llm/token.rs | 6 ++- crates/collab/src/rpc.rs | 4 ++ .../src/inline_completion_button.rs | 42 ++++++++++++++++++- crates/proto/proto/app.proto | 1 + crates/zeta/src/zeta.rs | 5 ++- 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/crates/agent/src/agent_panel.rs b/crates/agent/src/agent_panel.rs index 04832e856f..d4fc8823e2 100644 --- a/crates/agent/src/agent_panel.rs +++ b/crates/agent/src/agent_panel.rs @@ -1973,7 +1973,7 @@ impl AgentPanel { return None; } - if self.user_store.read(cx).current_user_account_too_young() { + if self.user_store.read(cx).account_too_young() { Some(self.render_young_account_upsell(cx).into_any_element()) } else { Some(self.render_trial_upsell(cx).into_any_element()) diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a61146404e..c5dbf27c51 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -109,6 +109,7 @@ pub struct UserStore { edit_predictions_usage_limit: Option, is_usage_based_billing_enabled: Option, account_too_young: Option, + has_overdue_invoices: Option, current_user: watch::Receiver>>, accepted_tos_at: Option>>, contacts: Vec>, @@ -176,6 +177,7 @@ impl UserStore { edit_predictions_usage_limit: None, is_usage_based_billing_enabled: None, account_too_young: None, + has_overdue_invoices: None, accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), @@ -350,6 +352,7 @@ impl UserStore { .and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0)); this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled; this.account_too_young = message.payload.account_too_young; + this.has_overdue_invoices = message.payload.has_overdue_invoices; if let Some(usage) = message.payload.usage { this.model_request_usage_amount = Some(usage.model_requests_usage_amount); @@ -755,11 +758,16 @@ impl UserStore { self.current_user.clone() } - /// Check if the current user's account is too new to use the service - pub fn current_user_account_too_young(&self) -> bool { + /// Returns whether the user's account is too new to use the service. + pub fn account_too_young(&self) -> bool { self.account_too_young.unwrap_or(false) } + /// Returns whether the current user has overdue invoices and usage should be blocked. + pub fn has_overdue_invoices(&self) -> bool { + self.has_overdue_invoices.unwrap_or(false) + } + pub fn current_user_has_accepted_terms(&self) -> Option { self.accepted_tos_at .map(|accepted_tos_at| accepted_tos_at.is_some()) diff --git a/crates/collab/src/llm/token.rs b/crates/collab/src/llm/token.rs index 8f78c2ff01..09405bdc91 100644 --- a/crates/collab/src/llm/token.rs +++ b/crates/collab/src/llm/token.rs @@ -1,5 +1,5 @@ use crate::db::billing_subscription::SubscriptionKind; -use crate::db::{billing_subscription, user}; +use crate::db::{billing_customer, billing_subscription, user}; use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; use crate::{Config, db::billing_preference}; use anyhow::{Context as _, Result}; @@ -32,6 +32,8 @@ pub struct LlmTokenClaims { pub enable_model_request_overages: bool, pub model_request_overages_spend_limit_in_cents: u32, pub can_use_web_search_tool: bool, + #[serde(default)] + pub has_overdue_invoices: bool, } const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60); @@ -40,6 +42,7 @@ impl LlmTokenClaims { pub fn create( user: &user::Model, is_staff: bool, + billing_customer: billing_customer::Model, billing_preferences: Option, feature_flags: &Vec, subscription: billing_subscription::Model, @@ -99,6 +102,7 @@ impl LlmTokenClaims { .map_or(0, |preferences| { preferences.model_request_overages_spend_limit_in_cents as u32 }), + has_overdue_invoices: billing_customer.has_overdue_invoices, }; Ok(jsonwebtoken::encode( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4bbb745f68..c35cf2e98b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2748,6 +2748,7 @@ async fn make_update_user_plan_message( Ok(proto::UpdateUserPlan { plan: plan.into(), trial_started_at: billing_customer + .as_ref() .and_then(|billing_customer| billing_customer.trial_started_at) .map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64), is_usage_based_billing_enabled: if is_staff { @@ -2762,6 +2763,8 @@ async fn make_update_user_plan_message( } }), account_too_young: Some(account_too_young), + has_overdue_invoices: billing_customer + .map(|billing_customer| billing_customer.has_overdue_invoices), usage: usage.map(|usage| { let plan = match plan { proto::Plan::Free => zed_llm_client::Plan::ZedFree, @@ -4077,6 +4080,7 @@ async fn get_llm_api_token( let token = LlmTokenClaims::create( &user, session.is_staff(), + billing_customer, billing_preferences, &flags, billing_subscription, diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index cbd2ade09d..b196436feb 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -745,7 +745,7 @@ impl InlineCompletionButton { }) }) .separator(); - } else if self.user_store.read(cx).current_user_account_too_young() { + } else if self.user_store.read(cx).account_too_young() { menu = menu .custom_entry( |_window, _cx| { @@ -785,6 +785,46 @@ impl InlineCompletionButton { }, ) .separator(); + } else if self.user_store.read(cx).has_overdue_invoices() { + menu = menu + .custom_entry( + |_window, _cx| { + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .size(IconSize::Small) + .color(Color::Warning), + ) + .child( + Label::new("You have an outstanding invoice") + .size(LabelSize::Small) + .color(Color::Warning), + ) + .into_any_element() + }, + |window, cx| { + window.dispatch_action( + Box::new(OpenZedUrl { + url: zed_urls::account_url(cx), + }), + cx, + ); + }, + ) + .entry( + "Check your payment status or contact us at billing-support@zed.dev to continue using this feature.", + None, + |window, cx| { + window.dispatch_action( + Box::new(OpenZedUrl { + url: zed_urls::account_url(cx), + }), + cx, + ); + }, + ) + .separator(); } self.build_language_settings_menu(menu, window, cx).when( diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index eea46385fc..5330ee506a 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -28,6 +28,7 @@ message UpdateUserPlan { optional SubscriptionUsage usage = 4; optional SubscriptionPeriod subscription_period = 5; optional bool account_too_young = 6; + optional bool has_overdue_invoices = 7; } message SubscriptionPeriod { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index fcbeeb56a6..f84ed5eb2f 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1578,8 +1578,9 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider .zeta .read(cx) .user_store - .read(cx) - .current_user_account_too_young() + .read_with(cx, |user_store, _| { + user_store.account_too_young() || user_store.has_overdue_invoices() + }) { return; }