ai_upsell_card.rs

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