Detailed changes
@@ -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<Pixels>,
zoomed: bool,
pending_serialization: Option<Task<Result<()>>>,
- hide_trial_upsell: bool,
- _trial_markdown: Entity<Markdown>,
+ 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<Self>,
@@ -1985,6 +1973,77 @@ 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<Self>) -> 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);
+ },
+ );
+
+ 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<Self>) -> impl IntoElement {
let checkbox = CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again").color(Color::Muted),
@@ -1992,7 +2051,7 @@ 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);
},
);
@@ -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";
}
@@ -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.
@@ -108,6 +108,7 @@ pub struct UserStore {
edit_predictions_usage_amount: Option<u32>,
edit_predictions_usage_limit: Option<proto::UsageLimit>,
is_usage_based_billing_enabled: Option<bool>,
+ account_too_young: Option<bool>,
current_user: watch::Receiver<Option<Arc<User>>>,
accepted_tos_at: Option<Option<DateTime<Utc>>>,
contacts: Vec<Arc<Contact>>,
@@ -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<bool> {
self.accepted_tos_at
.map(|accepted_tos_at| accepted_tos_at.is_some())
@@ -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,
@@ -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<Self>,
) -> Entity<ContextMenu> {
- 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::<PredictEditsRateCompletionsFeatureFlag>(),
|this| this.action("Rate Completions", RateCompletions.boxed_clone()),
@@ -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 {
@@ -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