ai onboarding: Add first-open upsell card (#35199)

Danilo Leal created

Release Notes:

- N/A

Change summary

crates/ai_onboarding/src/ai_onboarding.rs  |   3 
crates/ai_onboarding/src/ai_upsell_card.rs | 201 ++++++++++++++++++++++++
2 files changed, 204 insertions(+)

Detailed changes

crates/ai_onboarding/src/ai_onboarding.rs 🔗

@@ -1,12 +1,14 @@
 mod agent_api_keys_onboarding;
 mod agent_panel_onboarding_card;
 mod agent_panel_onboarding_content;
+mod ai_upsell_card;
 mod edit_prediction_onboarding_content;
 mod young_account_banner;
 
 pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
 pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
 pub use agent_panel_onboarding_content::AgentPanelOnboarding;
+pub use ai_upsell_card::AiUpsellCard;
 pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
 pub use young_account_banner::YoungAccountBanner;
 
@@ -54,6 +56,7 @@ impl RenderOnce for BulletItem {
     }
 }
 
+#[derive(PartialEq)]
 pub enum SignInStatus {
     SignedIn,
     SigningIn,

crates/ai_onboarding/src/ai_upsell_card.rs 🔗

@@ -0,0 +1,201 @@
+use std::sync::Arc;
+
+use client::{Client, zed_urls};
+use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
+use ui::{Divider, List, Vector, VectorName, prelude::*};
+
+use crate::{BulletItem, SignInStatus};
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct AiUpsellCard {
+    pub sign_in_status: SignInStatus,
+    pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
+}
+
+impl AiUpsellCard {
+    pub fn new(client: Arc<Client>) -> Self {
+        let status = *client.status().borrow();
+
+        Self {
+            sign_in_status: status.into(),
+            sign_in: Arc::new(move |_window, cx| {
+                cx.spawn({
+                    let client = client.clone();
+                    async move |cx| {
+                        client.authenticate_and_connect(true, cx).await;
+                    }
+                })
+                .detach();
+            }),
+        }
+    }
+}
+
+impl RenderOnce for AiUpsellCard {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let pro_section = v_flex()
+            .w_full()
+            .gap_1()
+            .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 with Claude models"))
+                    .child(BulletItem::new(
+                        "Unlimited edit predictions with Zeta, our open-source model",
+                    )),
+            );
+
+        let free_section = v_flex()
+            .w_full()
+            .gap_1()
+            .child(
+                h_flex()
+                    .gap_2()
+                    .child(
+                        Label::new("Free")
+                            .size(LabelSize::Small)
+                            .color(Color::Muted)
+                            .buffer_font(cx),
+                    )
+                    .child(Divider::horizontal()),
+            )
+            .child(
+                List::new()
+                    .child(BulletItem::new("50 prompts with the Claude models"))
+                    .child(BulletItem::new("2,000 accepted edit predictions")),
+            );
+
+        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.))
+                .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
+        );
+
+        let gradient_bg = div()
+            .absolute()
+            .inset_0()
+            .size_full()
+            .bg(gpui::linear_gradient(
+                180.,
+                gpui::linear_color_stop(
+                    cx.theme().colors().elevated_surface_background.opacity(0.8),
+                    0.,
+                ),
+                gpui::linear_color_stop(
+                    cx.theme().colors().elevated_surface_background.opacity(0.),
+                    0.8,
+                ),
+            ));
+
+        const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
+
+        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))
+                        }),
+                )
+                .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))
+                .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()
+            .relative()
+            .p_6()
+            .pt_4()
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .rounded_lg()
+            .overflow_hidden()
+            .child(grid_bg)
+            .child(gradient_bg)
+            .child(Headline::new("Try Zed AI"))
+            .child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
+            .child(
+                h_flex()
+                    .mt_1p5()
+                    .mb_2p5()
+                    .items_start()
+                    .gap_12()
+                    .child(free_section)
+                    .child(pro_section),
+            )
+            .child(footer_buttons)
+    }
+}
+
+impl Component for AiUpsellCard {
+    fn scope() -> ComponentScope {
+        ComponentScope::Agent
+    }
+
+    fn name() -> &'static str {
+        "AI Upsell Card"
+    }
+
+    fn sort_name() -> &'static str {
+        "AI Upsell Card"
+    }
+
+    fn description() -> Option<&'static str> {
+        Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
+    }
+
+    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+        Some(
+            v_flex()
+                .p_4()
+                .gap_4()
+                .children(vec![example_group(vec![
+                    single_example(
+                        "Signed Out State",
+                        AiUpsellCard {
+                            sign_in_status: SignInStatus::SignedOut,
+                            sign_in: Arc::new(|_, _| {}),
+                        }
+                        .into_any_element(),
+                    ),
+                    single_example(
+                        "Signed In State",
+                        AiUpsellCard {
+                            sign_in_status: SignInStatus::SignedIn,
+                            sign_in: Arc::new(|_, _| {}),
+                        }
+                        .into_any_element(),
+                    ),
+                ])])
+                .into_any_element(),
+        )
+    }
+}