ai_upsell_card.rs

  1use std::sync::Arc;
  2
  3use client::{Client, UserStore, zed_urls};
  4use cloud_llm_client::Plan;
  5use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
  6use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
  7
  8use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions};
  9
 10#[derive(IntoElement, RegisterComponent)]
 11pub struct AiUpsellCard {
 12    sign_in_status: SignInStatus,
 13    sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
 14    account_too_young: bool,
 15    user_plan: Option<Plan>,
 16    tab_index: Option<isize>,
 17}
 18
 19impl AiUpsellCard {
 20    pub fn new(
 21        client: Arc<Client>,
 22        user_store: &Entity<UserStore>,
 23        user_plan: Option<Plan>,
 24        cx: &mut App,
 25    ) -> Self {
 26        let status = *client.status().borrow();
 27        let store = user_store.read(cx);
 28
 29        Self {
 30            user_plan,
 31            sign_in_status: status.into(),
 32            sign_in: Arc::new(move |_window, cx| {
 33                cx.spawn({
 34                    let client = client.clone();
 35                    async move |cx| client.sign_in_with_optional_connect(true, cx).await
 36                })
 37                .detach_and_log_err(cx);
 38            }),
 39            account_too_young: store.account_too_young(),
 40            tab_index: None,
 41        }
 42    }
 43
 44    pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
 45        self.tab_index = tab_index;
 46        self
 47    }
 48}
 49
 50impl RenderOnce for AiUpsellCard {
 51    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 52        let plan_definitions = PlanDefinitions;
 53        let young_account_banner = YoungAccountBanner;
 54
 55        let pro_section = v_flex()
 56            .flex_grow()
 57            .w_full()
 58            .gap_1()
 59            .child(
 60                h_flex()
 61                    .gap_2()
 62                    .child(
 63                        Label::new("Pro")
 64                            .size(LabelSize::Small)
 65                            .color(Color::Accent)
 66                            .buffer_font(cx),
 67                    )
 68                    .child(Divider::horizontal()),
 69            )
 70            .child(plan_definitions.pro_plan(false));
 71
 72        let free_section = v_flex()
 73            .flex_grow()
 74            .w_full()
 75            .gap_1()
 76            .child(
 77                h_flex()
 78                    .gap_2()
 79                    .child(
 80                        Label::new("Free")
 81                            .size(LabelSize::Small)
 82                            .color(Color::Muted)
 83                            .buffer_font(cx),
 84                    )
 85                    .child(Divider::horizontal()),
 86            )
 87            .child(plan_definitions.free_plan());
 88
 89        let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
 90            Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
 91                .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
 92        );
 93
 94        let gradient_bg = div()
 95            .absolute()
 96            .inset_0()
 97            .size_full()
 98            .bg(gpui::linear_gradient(
 99                180.,
100                gpui::linear_color_stop(
101                    cx.theme().colors().elevated_surface_background.opacity(0.8),
102                    0.,
103                ),
104                gpui::linear_color_stop(
105                    cx.theme().colors().elevated_surface_background.opacity(0.),
106                    0.8,
107                ),
108            ));
109
110        let description = PlanDefinitions::AI_DESCRIPTION;
111
112        let card = v_flex()
113            .relative()
114            .flex_grow()
115            .p_4()
116            .pt_3()
117            .border_1()
118            .border_color(cx.theme().colors().border)
119            .rounded_lg()
120            .overflow_hidden()
121            .child(grid_bg)
122            .child(gradient_bg);
123
124        let plans_section = h_flex()
125            .w_full()
126            .mt_1p5()
127            .mb_2p5()
128            .items_start()
129            .gap_6()
130            .child(free_section)
131            .child(pro_section);
132
133        let footer_container = v_flex().items_center().gap_1();
134
135        let certified_user_stamp = div()
136            .absolute()
137            .top_2()
138            .right_2()
139            .size(rems_from_px(72.))
140            .child(
141                Vector::new(
142                    VectorName::ProUserStamp,
143                    rems_from_px(72.),
144                    rems_from_px(72.),
145                )
146                .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
147                .with_rotate_animation(10),
148            );
149
150        let pro_trial_stamp = div()
151            .absolute()
152            .top_2()
153            .right_2()
154            .size(rems_from_px(72.))
155            .child(
156                Vector::new(
157                    VectorName::ProTrialStamp,
158                    rems_from_px(72.),
159                    rems_from_px(72.),
160                )
161                .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
162            );
163
164        match self.sign_in_status {
165            SignInStatus::SignedIn => match self.user_plan {
166                None | Some(Plan::ZedFree) => card
167                    .child(Label::new("Try Zed AI").size(LabelSize::Large))
168                    .map(|this| {
169                        if self.account_too_young {
170                            this.child(young_account_banner).child(
171                                v_flex()
172                                    .mt_2()
173                                    .gap_1()
174                                    .child(
175                                        h_flex()
176                                            .gap_2()
177                                            .child(
178                                                Label::new("Pro")
179                                                    .size(LabelSize::Small)
180                                                    .color(Color::Accent)
181                                                    .buffer_font(cx),
182                                            )
183                                            .child(Divider::horizontal()),
184                                    )
185                                    .child(plan_definitions.pro_plan(true))
186                                    .child(
187                                        Button::new("pro", "Get Started")
188                                            .full_width()
189                                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
190                                            .on_click(move |_, _window, cx| {
191                                                telemetry::event!(
192                                                    "Upgrade To Pro Clicked",
193                                                    state = "young-account"
194                                                );
195                                                cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
196                                            }),
197                                    ),
198                            )
199                        } else {
200                            this.child(
201                                div()
202                                    .max_w_3_4()
203                                    .mb_2()
204                                    .child(Label::new(description).color(Color::Muted)),
205                            )
206                            .child(plans_section)
207                            .child(
208                                footer_container
209                                    .child(
210                                        Button::new("start_trial", "Start 14-day Free Pro Trial")
211                                            .full_width()
212                                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
213                                            .when_some(self.tab_index, |this, tab_index| {
214                                                this.tab_index(tab_index)
215                                            })
216                                            .on_click(move |_, _window, cx| {
217                                                telemetry::event!(
218                                                    "Start Trial Clicked",
219                                                    state = "post-sign-in"
220                                                );
221                                                cx.open_url(&zed_urls::start_trial_url(cx))
222                                            }),
223                                    )
224                                    .child(
225                                        Label::new("No credit card required")
226                                            .size(LabelSize::Small)
227                                            .color(Color::Muted),
228                                    ),
229                            )
230                        }
231                    }),
232                Some(Plan::ZedProTrial) => card
233                    .child(pro_trial_stamp)
234                    .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
235                    .child(
236                        Label::new("Here's what you get for the next 14 days:")
237                            .color(Color::Muted)
238                            .mb_2(),
239                    )
240                    .child(plan_definitions.pro_trial(false)),
241                Some(Plan::ZedPro) => card
242                    .child(certified_user_stamp)
243                    .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
244                    .child(
245                        Label::new("Here's what you get:")
246                            .color(Color::Muted)
247                            .mb_2(),
248                    )
249                    .child(plan_definitions.pro_plan(false)),
250            },
251            // Signed Out State
252            _ => card
253                .child(Label::new("Try Zed AI").size(LabelSize::Large))
254                .child(
255                    div()
256                        .max_w_3_4()
257                        .mb_2()
258                        .child(Label::new(description).color(Color::Muted)),
259                )
260                .child(plans_section)
261                .child(
262                    Button::new("sign_in", "Sign In")
263                        .full_width()
264                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
265                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
266                        .on_click({
267                            let callback = self.sign_in.clone();
268                            move |_, window, cx| {
269                                telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
270                                callback(window, cx)
271                            }
272                        }),
273                ),
274        }
275    }
276}
277
278impl Component for AiUpsellCard {
279    fn scope() -> ComponentScope {
280        ComponentScope::Onboarding
281    }
282
283    fn name() -> &'static str {
284        "AI Upsell Card"
285    }
286
287    fn sort_name() -> &'static str {
288        "AI Upsell Card"
289    }
290
291    fn description() -> Option<&'static str> {
292        Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
293    }
294
295    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
296        Some(
297            v_flex()
298                .gap_4()
299                .items_center()
300                .max_w_4_5()
301                .child(single_example(
302                    "Signed Out State",
303                    AiUpsellCard {
304                        sign_in_status: SignInStatus::SignedOut,
305                        sign_in: Arc::new(|_, _| {}),
306                        account_too_young: false,
307                        user_plan: None,
308                        tab_index: Some(0),
309                    }
310                    .into_any_element(),
311                ))
312                .child(example_group_with_title(
313                    "Signed In States",
314                    vec![
315                        single_example(
316                            "Free Plan",
317                            AiUpsellCard {
318                                sign_in_status: SignInStatus::SignedIn,
319                                sign_in: Arc::new(|_, _| {}),
320                                account_too_young: false,
321                                user_plan: Some(Plan::ZedFree),
322                                tab_index: Some(1),
323                            }
324                            .into_any_element(),
325                        ),
326                        single_example(
327                            "Free Plan but Young Account",
328                            AiUpsellCard {
329                                sign_in_status: SignInStatus::SignedIn,
330                                sign_in: Arc::new(|_, _| {}),
331                                account_too_young: true,
332                                user_plan: Some(Plan::ZedFree),
333                                tab_index: Some(1),
334                            }
335                            .into_any_element(),
336                        ),
337                        single_example(
338                            "Pro Trial",
339                            AiUpsellCard {
340                                sign_in_status: SignInStatus::SignedIn,
341                                sign_in: Arc::new(|_, _| {}),
342                                account_too_young: false,
343                                user_plan: Some(Plan::ZedProTrial),
344                                tab_index: Some(1),
345                            }
346                            .into_any_element(),
347                        ),
348                        single_example(
349                            "Pro Plan",
350                            AiUpsellCard {
351                                sign_in_status: SignInStatus::SignedIn,
352                                sign_in: Arc::new(|_, _| {}),
353                                account_too_young: false,
354                                user_plan: Some(Plan::ZedPro),
355                                tab_index: Some(1),
356                            }
357                            .into_any_element(),
358                        ),
359                    ],
360                ))
361                .into_any_element(),
362        )
363    }
364}