onboarding: Adjust the AI upsell card depending on user's state (#35658)

Danilo Leal created

Use includes centralizing what each plan delivers in one single file
(`plan_definitions.rs`).

Release Notes:

- N/A

Change summary

assets/images/certified_user_stamp.svg                |   0 
assets/images/pro_trial_stamp.svg                     |   0 
crates/agent_ui/src/ui/end_trial_upsell.rs            |  20 
crates/ai_onboarding/src/agent_api_keys_onboarding.rs |   6 
crates/ai_onboarding/src/ai_onboarding.rs             | 326 +++++-------
crates/ai_onboarding/src/ai_upsell_card.rs            | 229 ++++++--
crates/ai_onboarding/src/plan_definitions.rs          |  39 +
crates/ai_onboarding/src/young_account_banner.rs      |   1 
crates/ui/src/components/image.rs                     |  17 
crates/ui/src/components/list.rs                      |   2 
crates/ui/src/components/list/list_bullet_item.rs     |  40 +
11 files changed, 396 insertions(+), 284 deletions(-)

Detailed changes

crates/agent_ui/src/ui/end_trial_upsell.rs 🔗

@@ -1,9 +1,9 @@
 use std::sync::Arc;
 
-use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
+use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions};
 use client::zed_urls;
 use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
-use ui::{Divider, List, Tooltip, prelude::*};
+use ui::{Divider, Tooltip, prelude::*};
 
 #[derive(IntoElement, RegisterComponent)]
 pub struct EndTrialUpsell {
@@ -18,6 +18,8 @@ impl EndTrialUpsell {
 
 impl RenderOnce for EndTrialUpsell {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let plan_definitions = PlanDefinitions;
+
         let pro_section = v_flex()
             .gap_1()
             .child(
@@ -31,13 +33,7 @@ impl RenderOnce for EndTrialUpsell {
                     )
                     .child(Divider::horizontal()),
             )
-            .child(
-                List::new()
-                    .child(BulletItem::new("500 prompts with Claude models"))
-                    .child(BulletItem::new(
-                        "Unlimited edit predictions with Zeta, our open-source model",
-                    )),
-            )
+            .child(plan_definitions.pro_plan(false))
             .child(
                 Button::new("cta-button", "Upgrade to Zed Pro")
                     .full_width()
@@ -68,11 +64,7 @@ impl RenderOnce for EndTrialUpsell {
                     )
                     .child(Divider::horizontal()),
             )
-            .child(
-                List::new()
-                    .child(BulletItem::new("50 prompts with the Claude models"))
-                    .child(BulletItem::new("2,000 accepted edit predictions")),
-            );
+            .child(plan_definitions.free_plan());
 
         AgentPanelOnboardingCard::new()
             .child(Headline::new("Your Zed Pro Trial has expired"))

crates/ai_onboarding/src/agent_api_keys_onboarding.rs 🔗

@@ -1,8 +1,6 @@
 use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
 use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
-use ui::{Divider, List, prelude::*};
-
-use crate::BulletItem;
+use ui::{Divider, List, ListBulletItem, prelude::*};
 
 pub struct ApiKeysWithProviders {
     configured_providers: Vec<(IconName, SharedString)>,
@@ -128,7 +126,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
                     )
                     .child(Divider::horizontal()),
             )
-            .child(List::new().child(BulletItem::new(
+            .child(List::new().child(ListBulletItem::new(
                 "Add your own keys to use AI without signing in.",
             )))
             .child(

crates/ai_onboarding/src/ai_onboarding.rs 🔗

@@ -3,6 +3,7 @@ mod agent_panel_onboarding_card;
 mod agent_panel_onboarding_content;
 mod ai_upsell_card;
 mod edit_prediction_onboarding_content;
+mod plan_definitions;
 mod young_account_banner;
 
 pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
@@ -11,51 +12,14 @@ pub use agent_panel_onboarding_content::AgentPanelOnboarding;
 pub use ai_upsell_card::AiUpsellCard;
 use cloud_llm_client::Plan;
 pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
+pub use plan_definitions::PlanDefinitions;
 pub use young_account_banner::YoungAccountBanner;
 
 use std::sync::Arc;
 
 use client::{Client, UserStore, zed_urls};
-use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
-use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
-
-#[derive(IntoElement)]
-pub struct BulletItem {
-    label: SharedString,
-}
-
-impl BulletItem {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            label: label.into(),
-        }
-    }
-}
-
-impl RenderOnce for BulletItem {
-    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        let line_height = 0.85 * window.line_height();
-
-        ListItem::new("list-item")
-            .selectable(false)
-            .child(
-                h_flex()
-                    .w_full()
-                    .min_w_0()
-                    .gap_1()
-                    .items_start()
-                    .child(
-                        h_flex().h(line_height).justify_center().child(
-                            Icon::new(IconName::Dash)
-                                .size(IconSize::XSmall)
-                                .color(Color::Hidden),
-                        ),
-                    )
-                    .child(div().w_full().min_w_0().child(Label::new(self.label))),
-            )
-            .into_any_element()
-    }
-}
+use gpui::{AnyElement, Entity, IntoElement, ParentElement};
+use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*};
 
 #[derive(PartialEq)]
 pub enum SignInStatus {
@@ -130,107 +94,6 @@ impl ZedAiOnboarding {
         self
     }
 
-    fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
-        v_flex()
-            .mt_2()
-            .gap_1()
-            .child(
-                h_flex()
-                    .gap_2()
-                    .child(
-                        Label::new("Free")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .buffer_font(cx),
-                    )
-                    .child(
-                        Label::new("(Current Plan)")
-                            .size(LabelSize::Small)
-                            .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
-                            .buffer_font(cx),
-                    )
-                    .child(Divider::horizontal()),
-            )
-            .child(
-                List::new()
-                    .child(BulletItem::new("50 prompts per month with Claude models"))
-                    .child(BulletItem::new(
-                        "2,000 accepted edit predictions with Zeta, our open-source model",
-                    )),
-            )
-    }
-
-    fn pro_trial_definition(&self) -> impl IntoElement {
-        List::new()
-            .child(BulletItem::new("150 prompts with Claude models"))
-            .child(BulletItem::new(
-                "Unlimited accepted edit predictions with Zeta, our open-source model",
-            ))
-    }
-
-    fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
-        v_flex().mt_2().gap_1().map(|this| {
-            if self.account_too_young {
-                this.child(
-                    h_flex()
-                        .gap_2()
-                        .child(
-                            Label::new("Pro")
-                                .size(LabelSize::Small)
-                                .color(Color::Accent)
-                                .buffer_font(cx),
-                        )
-                        .child(Divider::horizontal()),
-                )
-                .child(
-                    List::new()
-                        .child(BulletItem::new("500 prompts per month with Claude models"))
-                        .child(BulletItem::new(
-                            "Unlimited accepted edit predictions with Zeta, our open-source model",
-                        ))
-                        .child(BulletItem::new("$20 USD per month")),
-                )
-                .child(
-                    Button::new("pro", "Get Started")
-                        .full_width()
-                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                        .on_click(move |_, _window, cx| {
-                            telemetry::event!("Upgrade To Pro Clicked", state = "young-account");
-                            cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
-                        }),
-                )
-            } else {
-                this.child(
-                    h_flex()
-                        .gap_2()
-                        .child(
-                            Label::new("Pro Trial")
-                                .size(LabelSize::Small)
-                                .color(Color::Accent)
-                                .buffer_font(cx),
-                        )
-                        .child(Divider::horizontal()),
-                )
-                .child(
-                    List::new()
-                        .child(self.pro_trial_definition())
-                        .child(BulletItem::new(
-                            "Try it out for 14 days for free, no credit card required",
-                        )),
-                )
-                .child(
-                    Button::new("pro", "Start Free Trial")
-                        .full_width()
-                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                        .on_click(move |_, _window, cx| {
-                            telemetry::event!("Start Trial Clicked", state = "post-sign-in");
-                            cx.open_url(&zed_urls::start_trial_url(cx))
-                        }),
-                )
-            }
-        })
-    }
-
     fn render_accept_terms_of_service(&self) -> AnyElement {
         v_flex()
             .gap_1()
@@ -269,6 +132,7 @@ impl ZedAiOnboarding {
 
     fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
         let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
+        let plan_definitions = PlanDefinitions;
 
         v_flex()
             .gap_1()
@@ -278,7 +142,7 @@ impl ZedAiOnboarding {
                     .color(Color::Muted)
                     .mb_2(),
             )
-            .child(self.pro_trial_definition())
+            .child(plan_definitions.pro_plan(false))
             .child(
                 Button::new("sign_in", "Try Zed Pro for Free")
                     .disabled(signing_in)
@@ -297,43 +161,132 @@ impl ZedAiOnboarding {
 
     fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
         let young_account_banner = YoungAccountBanner;
+        let plan_definitions = PlanDefinitions;
 
-        v_flex()
-            .relative()
-            .gap_1()
-            .child(Headline::new("Welcome to Zed AI"))
-            .map(|this| {
-                if self.account_too_young {
-                    this.child(young_account_banner)
-                } else {
-                    this.child(self.free_plan_definition(cx)).when_some(
-                        self.dismiss_onboarding.as_ref(),
-                        |this, dismiss_callback| {
-                            let callback = dismiss_callback.clone();
+        if self.account_too_young {
+            v_flex()
+                .relative()
+                .max_w_full()
+                .gap_1()
+                .child(Headline::new("Welcome to Zed AI"))
+                .child(young_account_banner)
+                .child(
+                    v_flex()
+                        .mt_2()
+                        .gap_1()
+                        .child(
+                            h_flex()
+                                .gap_2()
+                                .child(
+                                    Label::new("Pro")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Accent)
+                                        .buffer_font(cx),
+                                )
+                                .child(Divider::horizontal()),
+                        )
+                        .child(plan_definitions.pro_plan(true))
+                        .child(
+                            Button::new("pro", "Get Started")
+                                .full_width()
+                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                                .on_click(move |_, _window, cx| {
+                                    telemetry::event!(
+                                        "Upgrade To Pro Clicked",
+                                        state = "young-account"
+                                    );
+                                    cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
+                                }),
+                        ),
+                )
+                .into_any_element()
+        } else {
+            v_flex()
+                .relative()
+                .gap_1()
+                .child(Headline::new("Welcome to Zed AI"))
+                .child(
+                    v_flex()
+                        .mt_2()
+                        .gap_1()
+                        .child(
+                            h_flex()
+                                .gap_2()
+                                .child(
+                                    Label::new("Free")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Muted)
+                                        .buffer_font(cx),
+                                )
+                                .child(
+                                    Label::new("(Current Plan)")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Custom(
+                                            cx.theme().colors().text_muted.opacity(0.6),
+                                        ))
+                                        .buffer_font(cx),
+                                )
+                                .child(Divider::horizontal()),
+                        )
+                        .child(plan_definitions.free_plan()),
+                )
+                .when_some(
+                    self.dismiss_onboarding.as_ref(),
+                    |this, dismiss_callback| {
+                        let callback = dismiss_callback.clone();
 
-                            this.child(
-                                h_flex().absolute().top_0().right_0().child(
-                                    IconButton::new("dismiss_onboarding", IconName::Close)
-                                        .icon_size(IconSize::Small)
-                                        .tooltip(Tooltip::text("Dismiss"))
-                                        .on_click(move |_, window, cx| {
-                                            telemetry::event!(
-                                                "Banner Dismissed",
-                                                source = "AI Onboarding",
-                                            );
-                                            callback(window, cx)
-                                        }),
-                                ),
-                            )
-                        },
-                    )
-                }
-            })
-            .child(self.pro_plan_definition(cx))
-            .into_any_element()
+                        this.child(
+                            h_flex().absolute().top_0().right_0().child(
+                                IconButton::new("dismiss_onboarding", IconName::Close)
+                                    .icon_size(IconSize::Small)
+                                    .tooltip(Tooltip::text("Dismiss"))
+                                    .on_click(move |_, window, cx| {
+                                        telemetry::event!(
+                                            "Banner Dismissed",
+                                            source = "AI Onboarding",
+                                        );
+                                        callback(window, cx)
+                                    }),
+                            ),
+                        )
+                    },
+                )
+                .child(
+                    v_flex()
+                        .mt_2()
+                        .gap_1()
+                        .child(
+                            h_flex()
+                                .gap_2()
+                                .child(
+                                    Label::new("Pro Trial")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Accent)
+                                        .buffer_font(cx),
+                                )
+                                .child(Divider::horizontal()),
+                        )
+                        .child(plan_definitions.pro_trial(true))
+                        .child(
+                            Button::new("pro", "Start Free Trial")
+                                .full_width()
+                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                                .on_click(move |_, _window, cx| {
+                                    telemetry::event!(
+                                        "Start Trial Clicked",
+                                        state = "post-sign-in"
+                                    );
+                                    cx.open_url(&zed_urls::start_trial_url(cx))
+                                }),
+                        ),
+                )
+                .into_any_element()
+        }
     }
 
     fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
+        let plan_definitions = PlanDefinitions;
+
         v_flex()
             .relative()
             .gap_1()
@@ -343,13 +296,7 @@ impl ZedAiOnboarding {
                     .color(Color::Muted)
                     .mb_2(),
             )
-            .child(
-                List::new()
-                    .child(BulletItem::new("150 prompts with Claude models"))
-                    .child(BulletItem::new(
-                        "Unlimited edit predictions with Zeta, our open-source model",
-                    )),
-            )
+            .child(plan_definitions.pro_trial(false))
             .when_some(
                 self.dismiss_onboarding.as_ref(),
                 |this, dismiss_callback| {
@@ -374,6 +321,8 @@ impl ZedAiOnboarding {
     }
 
     fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
+        let plan_definitions = PlanDefinitions;
+
         v_flex()
             .gap_1()
             .child(Headline::new("Welcome to Zed Pro"))
@@ -382,13 +331,7 @@ impl ZedAiOnboarding {
                     .color(Color::Muted)
                     .mb_2(),
             )
-            .child(
-                List::new()
-                    .child(BulletItem::new("500 prompts with Claude models"))
-                    .child(BulletItem::new(
-                        "Unlimited edit predictions with Zeta, our open-source model",
-                    )),
-            )
+            .child(plan_definitions.pro_plan(false))
             .child(
                 Button::new("pro", "Continue with Zed Pro")
                     .full_width()
@@ -450,8 +393,9 @@ impl Component for ZedAiOnboarding {
 
         Some(
             v_flex()
-                .p_4()
                 .gap_4()
+                .items_center()
+                .max_w_4_5()
                 .children(vec![
                     single_example(
                         "Not Signed-in",
@@ -462,8 +406,8 @@ impl Component for ZedAiOnboarding {
                         onboarding(SignInStatus::SignedIn, false, None, false),
                     ),
                     single_example(
-                        "Account too young",
-                        onboarding(SignInStatus::SignedIn, false, None, true),
+                        "Young Account",
+                        onboarding(SignInStatus::SignedIn, true, None, true),
                     ),
                     single_example(
                         "Free Plan",

crates/ai_onboarding/src/ai_upsell_card.rs 🔗

@@ -1,11 +1,14 @@
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 
 use client::{Client, zed_urls};
 use cloud_llm_client::Plan;
-use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
-use ui::{Divider, List, Vector, VectorName, prelude::*};
+use gpui::{
+    Animation, AnimationExt, AnyElement, App, IntoElement, RenderOnce, Transformation, Window,
+    percentage,
+};
+use ui::{Divider, Vector, VectorName, prelude::*};
 
-use crate::{BulletItem, SignInStatus};
+use crate::{SignInStatus, plan_definitions::PlanDefinitions};
 
 #[derive(IntoElement, RegisterComponent)]
 pub struct AiUpsellCard {
@@ -36,6 +39,8 @@ impl AiUpsellCard {
 
 impl RenderOnce for AiUpsellCard {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let plan_definitions = PlanDefinitions;
+
         let pro_section = v_flex()
             .flex_grow()
             .w_full()
@@ -51,13 +56,7 @@ impl RenderOnce for AiUpsellCard {
                     )
                     .child(Divider::horizontal()),
             )
-            .child(
-                List::new()
-                    .child(BulletItem::new("500 prompts with Claude models"))
-                    .child(BulletItem::new(
-                        "Unlimited edit predictions with Zeta, our open-source model",
-                    )),
-            );
+            .child(plan_definitions.pro_plan(false));
 
         let free_section = v_flex()
             .flex_grow()
@@ -74,11 +73,7 @@ impl RenderOnce for AiUpsellCard {
                     )
                     .child(Divider::horizontal()),
             )
-            .child(
-                List::new()
-                    .child(BulletItem::new("50 prompts with Claude models"))
-                    .child(BulletItem::new("2,000 accepted edit predictions")),
-            );
+            .child(plan_definitions.free_plan());
 
         let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
             Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
@@ -101,44 +96,11 @@ impl RenderOnce for AiUpsellCard {
                 ),
             ));
 
-        const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
+        let description = PlanDefinitions::AI_DESCRIPTION;
 
-        let footer_buttons = match self.sign_in_status {
-            SignInStatus::SignedIn => v_flex()
-                .items_center()
-                .gap_1()
-                .child(
-                    Button::new("sign_in", "Start 14-day Free Pro Trial")
-                        .full_width()
-                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                        .on_click(move |_, _window, cx| {
-                            telemetry::event!("Start Trial Clicked", state = "post-sign-in");
-                            cx.open_url(&zed_urls::start_trial_url(cx))
-                        })
-                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
-                )
-                .child(
-                    Label::new("No credit card required")
-                        .size(LabelSize::Small)
-                        .color(Color::Muted),
-                )
-                .into_any_element(),
-            _ => Button::new("sign_in", "Sign In")
-                .full_width()
-                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
-                .on_click({
-                    let callback = self.sign_in.clone();
-                    move |_, window, cx| {
-                        telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
-                        callback(window, cx)
-                    }
-                })
-                .into_any_element(),
-        };
-
-        v_flex()
+        let card = v_flex()
             .relative()
+            .flex_grow()
             .p_4()
             .pt_3()
             .border_1()
@@ -146,25 +108,129 @@ impl RenderOnce for AiUpsellCard {
             .rounded_lg()
             .overflow_hidden()
             .child(grid_bg)
-            .child(gradient_bg)
-            .child(Label::new("Try Zed AI").size(LabelSize::Large))
+            .child(gradient_bg);
+
+        let plans_section = h_flex()
+            .w_full()
+            .mt_1p5()
+            .mb_2p5()
+            .items_start()
+            .gap_6()
+            .child(free_section)
+            .child(pro_section);
+
+        let footer_container = v_flex().items_center().gap_1();
+
+        let certified_user_stamp = div()
+            .absolute()
+            .top_2()
+            .right_2()
+            .size(rems_from_px(72.))
             .child(
-                div()
-                    .max_w_3_4()
-                    .mb_2()
-                    .child(Label::new(DESCRIPTION).color(Color::Muted)),
-            )
+                Vector::new(
+                    VectorName::CertifiedUserStamp,
+                    rems_from_px(72.),
+                    rems_from_px(72.),
+                )
+                .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
+                .with_animation(
+                    "loading_stamp",
+                    Animation::new(Duration::from_secs(10)).repeat(),
+                    |this, delta| this.transform(Transformation::rotate(percentage(delta))),
+                ),
+            );
+
+        let pro_trial_stamp = div()
+            .absolute()
+            .top_2()
+            .right_2()
+            .size(rems_from_px(72.))
             .child(
-                h_flex()
-                    .w_full()
-                    .mt_1p5()
-                    .mb_2p5()
-                    .items_start()
-                    .gap_6()
-                    .child(free_section)
-                    .child(pro_section),
-            )
-            .child(footer_buttons)
+                Vector::new(
+                    VectorName::ProTrialStamp,
+                    rems_from_px(72.),
+                    rems_from_px(72.),
+                )
+                .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
+            );
+
+        match self.sign_in_status {
+            SignInStatus::SignedIn => match self.user_plan {
+                None | Some(Plan::ZedFree) => card
+                    .child(Label::new("Try Zed AI").size(LabelSize::Large))
+                    .child(
+                        div()
+                            .max_w_3_4()
+                            .mb_2()
+                            .child(Label::new(description).color(Color::Muted)),
+                    )
+                    .child(plans_section)
+                    .child(
+                        footer_container
+                            .child(
+                                Button::new("start_trial", "Start 14-day Free Pro Trial")
+                                    .full_width()
+                                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                                    .when_some(self.tab_index, |this, tab_index| {
+                                        this.tab_index(tab_index)
+                                    })
+                                    .on_click(move |_, _window, cx| {
+                                        telemetry::event!(
+                                            "Start Trial Clicked",
+                                            state = "post-sign-in"
+                                        );
+                                        cx.open_url(&zed_urls::start_trial_url(cx))
+                                    }),
+                            )
+                            .child(
+                                Label::new("No credit card required")
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            ),
+                    ),
+                Some(Plan::ZedProTrial) => card
+                    .child(pro_trial_stamp)
+                    .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
+                    .child(
+                        Label::new("Here's what you get for the next 14 days:")
+                            .color(Color::Muted)
+                            .mb_2(),
+                    )
+                    .child(plan_definitions.pro_trial(false)),
+                Some(Plan::ZedPro) => card
+                    .child(certified_user_stamp)
+                    .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
+                    .child(
+                        Label::new("Here's what you get:")
+                            .color(Color::Muted)
+                            .mb_2(),
+                    )
+                    .child(plan_definitions.pro_plan(false)),
+            },
+            // Signed Out State
+            _ => card
+                .child(Label::new("Try Zed AI").size(LabelSize::Large))
+                .child(
+                    div()
+                        .max_w_3_4()
+                        .mb_2()
+                        .child(Label::new(description).color(Color::Muted)),
+                )
+                .child(plans_section)
+                .child(
+                    Button::new("sign_in", "Sign In")
+                        .full_width()
+                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
+                        .on_click({
+                            let callback = self.sign_in.clone();
+                            move |_, window, cx| {
+                                telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
+                                callback(window, cx)
+                            }
+                        }),
+                ),
+        }
     }
 }
 
@@ -188,7 +254,6 @@ impl Component for AiUpsellCard {
     fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
         Some(
             v_flex()
-                .p_4()
                 .gap_4()
                 .children(vec![example_group(vec![
                     single_example(
@@ -202,11 +267,31 @@ impl Component for AiUpsellCard {
                         .into_any_element(),
                     ),
                     single_example(
-                        "Signed In State",
+                        "Free Plan",
                         AiUpsellCard {
                             sign_in_status: SignInStatus::SignedIn,
                             sign_in: Arc::new(|_, _| {}),
-                            user_plan: None,
+                            user_plan: Some(Plan::ZedFree),
+                            tab_index: Some(1),
+                        }
+                        .into_any_element(),
+                    ),
+                    single_example(
+                        "Pro Trial",
+                        AiUpsellCard {
+                            sign_in_status: SignInStatus::SignedIn,
+                            sign_in: Arc::new(|_, _| {}),
+                            user_plan: Some(Plan::ZedProTrial),
+                            tab_index: Some(1),
+                        }
+                        .into_any_element(),
+                    ),
+                    single_example(
+                        "Pro Plan",
+                        AiUpsellCard {
+                            sign_in_status: SignInStatus::SignedIn,
+                            sign_in: Arc::new(|_, _| {}),
+                            user_plan: Some(Plan::ZedPro),
                             tab_index: Some(1),
                         }
                         .into_any_element(),

crates/ai_onboarding/src/plan_definitions.rs 🔗

@@ -0,0 +1,39 @@
+use gpui::{IntoElement, ParentElement};
+use ui::{List, ListBulletItem, prelude::*};
+
+/// Centralized definitions for Zed AI plans
+pub struct PlanDefinitions;
+
+impl PlanDefinitions {
+    pub const AI_DESCRIPTION: &'static str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
+
+    pub fn free_plan(&self) -> impl IntoElement {
+        List::new()
+            .child(ListBulletItem::new("50 prompts with Claude models"))
+            .child(ListBulletItem::new("2,000 accepted edit predictions"))
+    }
+
+    pub fn pro_trial(&self, period: bool) -> impl IntoElement {
+        List::new()
+            .child(ListBulletItem::new("150 prompts with Claude models"))
+            .child(ListBulletItem::new(
+                "Unlimited edit predictions with Zeta, our open-source model",
+            ))
+            .when(period, |this| {
+                this.child(ListBulletItem::new(
+                    "Try it out for 14 days for free, no credit card required",
+                ))
+            })
+    }
+
+    pub fn pro_plan(&self, price: bool) -> impl IntoElement {
+        List::new()
+            .child(ListBulletItem::new("500 prompts with Claude models"))
+            .child(ListBulletItem::new(
+                "Unlimited edit predictions with Zeta, our open-source model",
+            ))
+            .when(price, |this| {
+                this.child(ListBulletItem::new("$20 USD per month"))
+            })
+    }
+}

crates/ui/src/components/image.rs 🔗

@@ -1,5 +1,6 @@
 use std::sync::Arc;
 
+use gpui::Transformation;
 use gpui::{App, IntoElement, Rems, RenderOnce, Size, Styled, Window, svg};
 use serde::{Deserialize, Serialize};
 use strum::{EnumIter, EnumString, IntoStaticStr};
@@ -12,11 +13,13 @@ use crate::prelude::*;
 )]
 #[strum(serialize_all = "snake_case")]
 pub enum VectorName {
-    ZedLogo,
-    ZedXCopilot,
-    Grid,
     AiGrid,
+    CertifiedUserStamp,
     DebuggerGrid,
+    Grid,
+    ProTrialStamp,
+    ZedLogo,
+    ZedXCopilot,
 }
 
 impl VectorName {
@@ -37,6 +40,7 @@ pub struct Vector {
     path: Arc<str>,
     color: Color,
     size: Size<Rems>,
+    transformation: Transformation,
 }
 
 impl Vector {
@@ -46,6 +50,7 @@ impl Vector {
             path: vector.path(),
             color: Color::default(),
             size: Size { width, height },
+            transformation: Transformation::default(),
         }
     }
 
@@ -66,6 +71,11 @@ impl Vector {
         self.size = size;
         self
     }
+
+    pub fn transform(mut self, transformation: Transformation) -> Self {
+        self.transformation = transformation;
+        self
+    }
 }
 
 impl RenderOnce for Vector {
@@ -81,6 +91,7 @@ impl RenderOnce for Vector {
             .h(height)
             .path(self.path)
             .text_color(self.color.color(cx))
+            .with_transformation(self.transformation)
     }
 }
 

crates/ui/src/components/list.rs 🔗

@@ -1,10 +1,12 @@
 mod list;
+mod list_bullet_item;
 mod list_header;
 mod list_item;
 mod list_separator;
 mod list_sub_header;
 
 pub use list::*;
+pub use list_bullet_item::*;
 pub use list_header::*;
 pub use list_item::*;
 pub use list_separator::*;

crates/ui/src/components/list/list_bullet_item.rs 🔗

@@ -0,0 +1,40 @@
+use crate::{ListItem, prelude::*};
+use gpui::{IntoElement, ParentElement, SharedString};
+
+#[derive(IntoElement)]
+pub struct ListBulletItem {
+    label: SharedString,
+}
+
+impl ListBulletItem {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+        }
+    }
+}
+
+impl RenderOnce for ListBulletItem {
+    fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        let line_height = 0.85 * window.line_height();
+
+        ListItem::new("list-item")
+            .selectable(false)
+            .child(
+                h_flex()
+                    .w_full()
+                    .min_w_0()
+                    .gap_1()
+                    .items_start()
+                    .child(
+                        h_flex().h(line_height).justify_center().child(
+                            Icon::new(IconName::Dash)
+                                .size(IconSize::XSmall)
+                                .color(Color::Hidden),
+                        ),
+                    )
+                    .child(div().w_full().min_w_0().child(Label::new(self.label))),
+            )
+            .into_any_element()
+    }
+}