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