Better messaging for accounts that are too young (#31212)
Right now you find this out the first time you try and submit a completion. These changes communicate much earlier to the user what the issue is with their account and what they can do about it. Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
parent
9f7987c532
commit
508ccde363
7 changed files with 207 additions and 77 deletions
|
@ -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,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<Self>) -> 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<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);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue