diff --git a/crates/agent/src/agent_panel.rs b/crates/agent/src/agent_panel.rs index 713ae5c8ab..04832e856f 100644 --- a/crates/agent/src/agent_panel.rs +++ b/crates/agent/src/agent_panel.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use std::time::Duration; use db::kvp::{Dismissable, KEY_VALUE_STORE}; -use markdown::Markdown; use serde::{Deserialize, Serialize}; use anyhow::{Result, anyhow}; @@ -157,7 +156,7 @@ pub fn init(cx: &mut App) { window.refresh(); }) .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| { - TrialUpsell::set_dismissed(false, cx); + Upsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| { TrialEndUpsell::set_dismissed(false, cx); @@ -370,8 +369,7 @@ pub struct AgentPanel { height: Option, zoomed: bool, pending_serialization: Option>>, - hide_trial_upsell: bool, - _trial_markdown: Entity, + hide_upsell: bool, } impl AgentPanel { @@ -676,15 +674,6 @@ impl AgentPanel { }, ); - let trial_markdown = cx.new(|cx| { - Markdown::new( - include_str!("trial_markdown.md").into(), - Some(language_registry.clone()), - None, - cx, - ) - }); - Self { active_view, workspace, @@ -721,8 +710,7 @@ impl AgentPanel { height: None, zoomed: false, pending_serialization: None, - hide_trial_upsell: false, - _trial_markdown: trial_markdown, + hide_upsell: false, } } @@ -1946,7 +1934,7 @@ impl AgentPanel { return false; } - if self.hide_trial_upsell || TrialUpsell::dismissed() { + if self.hide_upsell || Upsell::dismissed() { return false; } @@ -1976,7 +1964,7 @@ impl AgentPanel { true } - fn render_trial_upsell( + fn render_upsell( &self, _window: &mut Window, cx: &mut Context, @@ -1985,6 +1973,14 @@ impl AgentPanel { return None; } + if self.user_store.read(cx).current_user_account_too_young() { + Some(self.render_young_account_upsell(cx).into_any_element()) + } else { + Some(self.render_trial_upsell(cx).into_any_element()) + } + } + + fn render_young_account_upsell(&self, cx: &mut Context) -> impl IntoElement { let checkbox = CheckboxWithLabel::new( "dont-show-again", Label::new("Don't show again").color(Color::Muted), @@ -1992,7 +1988,70 @@ impl AgentPanel { move |toggle_state, _window, cx| { let toggle_state_bool = toggle_state.selected(); - TrialUpsell::set_dismissed(toggle_state_bool, cx); + Upsell::set_dismissed(toggle_state_bool, cx); + }, + ); + + let contents = div() + .size_full() + .gap_2() + .flex() + .flex_col() + .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small)) + .child( + Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.") + .size(LabelSize::Small), + ) + .child( + Label::new( + "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.", + ) + .color(Color::Muted), + ) + .child( + h_flex() + .w_full() + .px_neg_1() + .justify_between() + .items_center() + .child(h_flex().items_center().gap_1().child(checkbox)) + .child( + h_flex() + .gap_2() + .child( + Button::new("dismiss-button", "Not Now") + .style(ButtonStyle::Transparent) + .color(Color::Muted) + .on_click({ + let agent_panel = cx.entity(); + move |_, _, cx| { + agent_panel.update(cx, |this, cx| { + this.hide_upsell = true; + cx.notify(); + }); + } + }), + ) + .child( + Button::new("cta-button", "Upgrade to Zed Pro") + .style(ButtonStyle::Transparent) + .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), + ), + ), + ); + + self.render_upsell_container(cx, contents) + } + + fn render_trial_upsell(&self, cx: &mut Context) -> impl IntoElement { + let checkbox = CheckboxWithLabel::new( + "dont-show-again", + Label::new("Don't show again").color(Color::Muted), + ToggleState::Unselected, + move |toggle_state, _window, cx| { + let toggle_state_bool = toggle_state.selected(); + + Upsell::set_dismissed(toggle_state_bool, cx); }, ); @@ -2030,7 +2089,7 @@ impl AgentPanel { let agent_panel = cx.entity(); move |_, _, cx| { agent_panel.update(cx, |this, cx| { - this.hide_trial_upsell = true; + this.hide_upsell = true; cx.notify(); }); } @@ -2044,7 +2103,7 @@ impl AgentPanel { ), ); - Some(self.render_upsell_container(cx, contents)) + self.render_upsell_container(cx, contents) } fn render_trial_end_upsell( @@ -2910,7 +2969,7 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::reset_font_size)) .on_action(cx.listener(Self::toggle_zoom)) .child(self.render_toolbar(window, cx)) - .children(self.render_trial_upsell(window, cx)) + .children(self.render_upsell(window, cx)) .children(self.render_trial_end_upsell(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { .. } => parent @@ -3099,9 +3158,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { } } -struct TrialUpsell; +struct Upsell; -impl Dismissable for TrialUpsell { +impl Dismissable for Upsell { const KEY: &'static str = "dismissed-trial-upsell"; } diff --git a/crates/agent/src/trial_markdown.md b/crates/agent/src/trial_markdown.md deleted file mode 100644 index b2a6e515cd..0000000000 --- a/crates/agent/src/trial_markdown.md +++ /dev/null @@ -1,3 +0,0 @@ -# Build better with Zed Pro - -Try [Zed Pro](https://zed.dev/pricing) for free for 14 days - no credit card required. Only $20/month afterward. Cancel anytime. diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d9526ed6cf..a61146404e 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -108,6 +108,7 @@ pub struct UserStore { edit_predictions_usage_amount: Option, edit_predictions_usage_limit: Option, is_usage_based_billing_enabled: Option, + account_too_young: Option, current_user: watch::Receiver>>, accepted_tos_at: Option>>, contacts: Vec>, @@ -174,6 +175,7 @@ impl UserStore { edit_predictions_usage_amount: None, edit_predictions_usage_limit: None, is_usage_based_billing_enabled: None, + account_too_young: None, accepted_tos_at: None, contacts: Default::default(), incoming_contact_requests: Default::default(), @@ -347,6 +349,7 @@ impl UserStore { .trial_started_at .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; if let Some(usage) = message.payload.usage { this.model_request_usage_amount = Some(usage.model_requests_usage_amount); @@ -752,6 +755,11 @@ 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 { + self.account_too_young.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/rpc.rs b/crates/collab/src/rpc.rs index 2b488a7daf..26413bb9bb 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2716,6 +2716,7 @@ async fn make_update_user_plan_message( let plan = current_plan(db, user_id, is_staff).await?; let billing_customer = db.get_billing_customer_by_user_id(user_id).await?; let billing_preferences = db.get_billing_preferences(user_id).await?; + let user = db.get_user_by_id(user_id).await?; let (subscription_period, usage) = if let Some(llm_db) = llm_db { let subscription = db.get_active_billing_subscription(user_id).await?; @@ -2736,6 +2737,18 @@ async fn make_update_user_plan_message( (None, None) }; + // Calculate account_too_young + let account_too_young = if matches!(plan, proto::Plan::ZedPro) { + // If they have paid, then we allow them to use all of the features + false + } else if let Some(user) = user { + // If we have access to the profile age, we use that + chrono::Utc::now().naive_utc() - user.account_created_at() < MIN_ACCOUNT_AGE_FOR_LLM_USE + } else { + // Default to false otherwise + false + }; + Ok(proto::UpdateUserPlan { plan: plan.into(), trial_started_at: billing_customer @@ -2752,6 +2765,7 @@ async fn make_update_user_plan_message( ended_at: ended_at.timestamp() as u64, } }), + account_too_young: Some(account_too_young), usage: usage.map(|usage| { let plan = match plan { proto::Plan::Free => zed_llm_client::Plan::ZedFree, diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 150b9cdacf..cbd2ade09d 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -420,56 +420,6 @@ impl InlineCompletionButton { let fs = self.fs.clone(); let line_height = window.line_height(); - if let Some(usage) = self - .edit_prediction_provider - .as_ref() - .and_then(|provider| provider.usage(cx)) - { - menu = menu.header("Usage"); - menu = menu - .custom_entry( - move |_window, cx| { - let used_percentage = match usage.limit { - UsageLimit::Limited(limit) => { - Some((usage.amount as f32 / limit as f32) * 100.) - } - UsageLimit::Unlimited => None, - }; - - h_flex() - .flex_1() - .gap_1p5() - .children( - used_percentage - .map(|percent| ProgressBar::new("usage", percent, 100., cx)), - ) - .child( - Label::new(match usage.limit { - UsageLimit::Limited(limit) => { - format!("{} / {limit}", usage.amount) - } - UsageLimit::Unlimited => format!("{} / ∞", usage.amount), - }) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }, - move |_, cx| cx.open_url(&zed_urls::account_url(cx)), - ) - .when(usage.over_limit(), |menu| -> ContextMenu { - menu.entry("Subscribe to increase your limit", None, |window, cx| { - window.dispatch_action( - Box::new(OpenZedUrl { - url: zed_urls::account_url(cx), - }), - cx, - ); - }) - }) - .separator(); - } - menu = menu.header("Show Edit Predictions For"); let language_state = self.language.as_ref().map(|language| { @@ -745,7 +695,98 @@ impl InlineCompletionButton { window: &mut Window, cx: &mut Context, ) -> Entity { - ContextMenu::build(window, cx, |menu, window, cx| { + ContextMenu::build(window, cx, |mut menu, window, cx| { + if let Some(usage) = self + .edit_prediction_provider + .as_ref() + .and_then(|provider| provider.usage(cx)) + { + menu = menu.header("Usage"); + menu = menu + .custom_entry( + move |_window, cx| { + let used_percentage = match usage.limit { + UsageLimit::Limited(limit) => { + Some((usage.amount as f32 / limit as f32) * 100.) + } + UsageLimit::Unlimited => None, + }; + + h_flex() + .flex_1() + .gap_1p5() + .children( + used_percentage.map(|percent| { + ProgressBar::new("usage", percent, 100., cx) + }), + ) + .child( + Label::new(match usage.limit { + UsageLimit::Limited(limit) => { + format!("{} / {limit}", usage.amount) + } + UsageLimit::Unlimited => format!("{} / ∞", usage.amount), + }) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + move |_, cx| cx.open_url(&zed_urls::account_url(cx)), + ) + .when(usage.over_limit(), |menu| -> ContextMenu { + menu.entry("Subscribe to increase your limit", None, |window, cx| { + window.dispatch_action( + Box::new(OpenZedUrl { + url: zed_urls::account_url(cx), + }), + cx, + ); + }) + }) + .separator(); + } else if self.user_store.read(cx).current_user_account_too_young() { + menu = menu + .custom_entry( + |_window, _cx| { + h_flex() + .gap_1() + .child( + Icon::new(IconName::Warning) + .size(IconSize::Small) + .color(Color::Warning), + ) + .child( + Label::new("Your GitHub account is less than 30 days old") + .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( + "You need to upgrade to Zed Pro or contact us.", + 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( cx.has_flag::(), |this| this.action("Rate Completions", RateCompletions.boxed_clone()), diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 6b142906d4..eea46385fc 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -27,6 +27,7 @@ message UpdateUserPlan { optional bool is_usage_based_billing_enabled = 3; optional SubscriptionUsage usage = 4; optional SubscriptionPeriod subscription_period = 5; + optional bool account_too_young = 6; } message SubscriptionPeriod { diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 0eefe47540..fcbeeb56a6 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1574,6 +1574,16 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider return; } + if self + .zeta + .read(cx) + .user_store + .read(cx) + .current_user_account_too_young() + { + return; + } + if let Some(current_completion) = self.current_completion.as_ref() { let snapshot = buffer.read(cx).snapshot(); if current_completion