diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1a1ce35152..3ebba7e8eb 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -11,7 +11,7 @@ use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use text::ReplicaId; -use util::TryFutureExt as _; +use util::{TryFutureExt as _, maybe}; pub type UserId = u64; @@ -101,6 +101,7 @@ pub struct UserStore { participant_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, current_plan: Option, + subscription_period: Option<(DateTime, DateTime)>, trial_started_at: Option>, model_request_usage_amount: Option, model_request_usage_limit: Option, @@ -166,6 +167,7 @@ impl UserStore { by_github_login: Default::default(), current_user: current_user_rx, current_plan: None, + subscription_period: None, trial_started_at: None, model_request_usage_amount: None, model_request_usage_limit: None, @@ -333,6 +335,13 @@ impl UserStore { ) -> Result<()> { this.update(&mut cx, |this, cx| { this.current_plan = Some(message.payload.plan()); + this.subscription_period = maybe!({ + let period = message.payload.subscription_period?; + let started_at = DateTime::from_timestamp(period.started_at as i64, 0)?; + let ended_at = DateTime::from_timestamp(period.ended_at as i64, 0)?; + + Some((started_at, ended_at)) + }); this.trial_started_at = message .payload .trial_started_at @@ -713,6 +722,10 @@ impl UserStore { self.current_plan } + pub fn subscription_period(&self) -> Option<(DateTime, DateTime)> { + self.subscription_period + } + pub fn trial_started_at(&self) -> Option> { self.trial_started_at } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 61daff9757..22972e62e1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2709,7 +2709,7 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> { let billing_customer = db.get_billing_customer_by_user_id(user_id).await?; let billing_preferences = db.get_billing_preferences(user_id).await?; - let usage = if let Some(llm_db) = session.app_state.llm_db.clone() { + let (subscription_period, usage) = if let Some(llm_db) = session.app_state.llm_db.clone() { let subscription = db.get_active_billing_subscription(user_id).await?; let subscription_period = crate::db::billing_subscription::Model::current_period( @@ -2717,15 +2717,17 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> { session.is_staff(), ); - if let Some((period_start_at, period_end_at)) = subscription_period { + let usage = if let Some((period_start_at, period_end_at)) = subscription_period { llm_db .get_subscription_usage_for_period(user_id, period_start_at, period_end_at) .await? } else { None - } + }; + + (subscription_period, usage) } else { - None + (None, None) }; session @@ -2743,6 +2745,12 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> { billing_preferences .map(|preferences| preferences.model_request_overages_enabled) }, + subscription_period: subscription_period.map(|(started_at, ended_at)| { + proto::SubscriptionPeriod { + started_at: started_at.timestamp() as u64, + ended_at: ended_at.timestamp() as u64, + } + }), usage: usage.map(|usage| { let plan = match plan { proto::Plan::Free => zed_llm_client::Plan::Free, diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 5b83308220..c60b6606b5 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -2,7 +2,7 @@ use anthropic::{AnthropicModelMode, parse_prompt_too_long}; use anyhow::{Result, anyhow}; use client::{Client, UserStore, zed_urls}; use collections::BTreeMap; -use feature_flags::{FeatureFlagAppExt, LlmClosedBetaFeatureFlag, ZedProFeatureFlag}; +use feature_flags::{FeatureFlagAppExt, LlmClosedBetaFeatureFlag}; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream, }; @@ -1036,48 +1036,56 @@ impl ConfigurationView { impl Render for ConfigurationView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - const ZED_AI_URL: &str = "https://zed.dev/ai"; + const ZED_PRICING_URL: &str = "https://zed.dev/pricing"; let is_connected = !self.state.read(cx).is_signed_out(); - let plan = self.state.read(cx).user_store.read(cx).current_plan(); + let user_store = self.state.read(cx).user_store.read(cx); + let plan = user_store.current_plan(); + let subscription_period = user_store.subscription_period(); + let eligible_for_trial = user_store.trial_started_at().is_none(); let has_accepted_terms = self.state.read(cx).has_accepted_terms_of_service(cx); let is_pro = plan == Some(proto::Plan::ZedPro); - let subscription_text = Label::new(if is_pro { - "You have access to Zed's hosted LLMs through your Zed Pro subscription." + let subscription_text = match (plan, subscription_period) { + (Some(proto::Plan::ZedPro), Some(_)) => { + "You have access to Zed's hosted LLMs through your Zed Pro subscription." + } + (Some(proto::Plan::ZedProTrial), Some(_)) => { + "You have access to Zed's hosted LLMs through your Zed Pro trial." + } + (Some(proto::Plan::Free), Some(_)) => { + "You have basic access to Zed's hosted LLMs through your Zed Free subscription." + } + _ => { + if eligible_for_trial { + "Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial." + } else { + "Subscribe for access to Zed's hosted LLMs." + } + } + }; + let manage_subscription_buttons = if is_pro { + h_flex().child( + Button::new("manage_settings", "Manage Subscription") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .on_click(cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx)))), + ) } else { - "You have basic access to models from Anthropic through the Zed AI Free plan." - }); - let manage_subscription_button = if is_pro { - Some( - h_flex().child( - Button::new("manage_settings", "Manage Subscription") - .style(ButtonStyle::Tinted(TintColor::Accent)) + h_flex() + .gap_2() + .child( + Button::new("learn_more", "Learn more") + .style(ButtonStyle::Subtle) + .on_click(cx.listener(|_, _, _, cx| cx.open_url(ZED_PRICING_URL))), + ) + .child( + Button::new("upgrade", "Upgrade") + .style(ButtonStyle::Subtle) + .color(Color::Accent) .on_click( cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx))), ), - ), - ) - } else if cx.has_flag::() { - Some( - h_flex() - .gap_2() - .child( - Button::new("learn_more", "Learn more") - .style(ButtonStyle::Subtle) - .on_click(cx.listener(|_, _, _, cx| cx.open_url(ZED_AI_URL))), - ) - .child( - Button::new("upgrade", "Upgrade") - .style(ButtonStyle::Subtle) - .color(Color::Accent) - .on_click( - cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx))), - ), - ), - ) - } else { - None + ) }; if is_connected { @@ -1091,7 +1099,7 @@ impl Render for ConfigurationView { )) .when(has_accepted_terms, |this| { this.child(subscription_text) - .children(manage_subscription_button) + .child(manage_subscription_buttons) }) } else { v_flex() diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index fd187416b1..6b142906d4 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -26,6 +26,12 @@ message UpdateUserPlan { optional uint64 trial_started_at = 2; optional bool is_usage_based_billing_enabled = 3; optional SubscriptionUsage usage = 4; + optional SubscriptionPeriod subscription_period = 5; +} + +message SubscriptionPeriod { + uint64 started_at = 1; + uint64 ended_at = 2; } message SubscriptionUsage {