ai_upsell_card.rs

  1use std::sync::Arc;
  2
  3use client::{Client, UserStore, zed_urls};
  4use cloud_api_types::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 pro_section = v_flex()
 53            .flex_grow()
 54            .w_full()
 55            .gap_1()
 56            .child(
 57                h_flex()
 58                    .gap_2()
 59                    .child(
 60                        Label::new("Pro")
 61                            .size(LabelSize::Small)
 62                            .color(Color::Accent)
 63                            .buffer_font(cx),
 64                    )
 65                    .child(Divider::horizontal()),
 66            )
 67            .child(PlanDefinitions.pro_plan());
 68
 69        let free_section = v_flex()
 70            .flex_grow()
 71            .w_full()
 72            .gap_1()
 73            .child(
 74                h_flex()
 75                    .gap_2()
 76                    .child(
 77                        Label::new("Free")
 78                            .size(LabelSize::Small)
 79                            .color(Color::Muted)
 80                            .buffer_font(cx),
 81                    )
 82                    .child(Divider::horizontal()),
 83            )
 84            .child(PlanDefinitions.free_plan());
 85
 86        let grid_bg = h_flex()
 87            .absolute()
 88            .inset_0()
 89            .w_full()
 90            .h(px(240.))
 91            .bg(gpui::pattern_slash(
 92                cx.theme().colors().border.opacity(0.1),
 93                2.,
 94                25.,
 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_rotate_animation(10),
151            );
152
153        let pro_trial_stamp = div()
154            .absolute()
155            .top_2()
156            .right_2()
157            .size(rems_from_px(72.))
158            .child(
159                Vector::new(
160                    VectorName::ProTrialStamp,
161                    rems_from_px(72.),
162                    rems_from_px(72.),
163                )
164                .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
165            );
166
167        match self.sign_in_status {
168            SignInStatus::SignedIn => match self.user_plan {
169                None | Some(Plan::ZedFree) => card
170                    .child(Label::new("Try Zed AI").size(LabelSize::Large))
171                    .map(|this| {
172                        if self.account_too_young {
173                            this.child(YoungAccountBanner).child(
174                                v_flex()
175                                    .mt_2()
176                                    .gap_1()
177                                    .child(
178                                        h_flex()
179                                            .gap_2()
180                                            .child(
181                                                Label::new("Pro")
182                                                    .size(LabelSize::Small)
183                                                    .color(Color::Accent)
184                                                    .buffer_font(cx),
185                                            )
186                                            .child(Divider::horizontal()),
187                                    )
188                                    .child(PlanDefinitions.pro_plan())
189                                    .child(
190                                        Button::new("pro", "Get Started")
191                                            .full_width()
192                                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
193                                            .on_click(move |_, _window, cx| {
194                                                telemetry::event!(
195                                                    "Upgrade To Pro Clicked",
196                                                    state = "young-account"
197                                                );
198                                                cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
199                                            }),
200                                    ),
201                            )
202                        } else {
203                            this.child(
204                                div()
205                                    .max_w_3_4()
206                                    .mb_2()
207                                    .child(Label::new(description).color(Color::Muted)),
208                            )
209                            .child(plans_section)
210                            .child(
211                                footer_container
212                                    .child(
213                                        Button::new("start_trial", "Start Pro Trial")
214                                            .full_width()
215                                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
216                                            .when_some(self.tab_index, |this, tab_index| {
217                                                this.tab_index(tab_index)
218                                            })
219                                            .on_click(move |_, _window, cx| {
220                                                telemetry::event!(
221                                                    "Start Trial Clicked",
222                                                    state = "post-sign-in"
223                                                );
224                                                cx.open_url(&zed_urls::start_trial_url(cx))
225                                            }),
226                                    )
227                                    .child(
228                                        Label::new("14 days, no credit card required")
229                                            .size(LabelSize::Small)
230                                            .color(Color::Muted),
231                                    ),
232                            )
233                        }
234                    }),
235                Some(Plan::ZedProTrial) => card
236                    .child(pro_trial_stamp)
237                    .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
238                    .child(
239                        Label::new("Here's what you get for the next 14 days:")
240                            .color(Color::Muted)
241                            .mb_2(),
242                    )
243                    .child(PlanDefinitions.pro_trial(false)),
244                Some(Plan::ZedPro) => card
245                    .child(certified_user_stamp)
246                    .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
247                    .child(
248                        Label::new("Here's what you get:")
249                            .color(Color::Muted)
250                            .mb_2(),
251                    )
252                    .child(PlanDefinitions.pro_plan()),
253                Some(Plan::ZedBusiness) => card
254                    .child(certified_user_stamp)
255                    .child(Label::new("You're in the Zed Business plan").size(LabelSize::Large))
256                    .child(
257                        Label::new("Here's what you get:")
258                            .color(Color::Muted)
259                            .mb_2(),
260                    )
261                    .child(PlanDefinitions.business_plan()),
262                Some(Plan::ZedStudent) => card
263                    .child(certified_user_stamp)
264                    .child(Label::new("You're in the Zed Student plan").size(LabelSize::Large))
265                    .child(
266                        Label::new("Here's what you get:")
267                            .color(Color::Muted)
268                            .mb_2(),
269                    )
270                    .child(PlanDefinitions.student_plan()),
271            },
272            // Signed Out State
273            _ => card
274                .child(Label::new("Try Zed AI").size(LabelSize::Large))
275                .child(
276                    div()
277                        .max_w_3_4()
278                        .mb_2()
279                        .child(Label::new(description).color(Color::Muted)),
280                )
281                .child(plans_section)
282                .child(
283                    Button::new("sign_in", "Sign In")
284                        .full_width()
285                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
286                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
287                        .on_click({
288                            let callback = self.sign_in.clone();
289                            move |_, window, cx| {
290                                telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
291                                callback(window, cx)
292                            }
293                        }),
294                ),
295        }
296    }
297}
298
299impl Component for AiUpsellCard {
300    fn scope() -> ComponentScope {
301        ComponentScope::Onboarding
302    }
303
304    fn name() -> &'static str {
305        "AI Upsell Card"
306    }
307
308    fn sort_name() -> &'static str {
309        "AI Upsell Card"
310    }
311
312    fn description() -> Option<&'static str> {
313        Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
314    }
315
316    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
317        Some(
318            v_flex()
319                .gap_4()
320                .items_center()
321                .max_w_4_5()
322                .child(single_example(
323                    "Signed Out State",
324                    AiUpsellCard {
325                        sign_in_status: SignInStatus::SignedOut,
326                        sign_in: Arc::new(|_, _| {}),
327                        account_too_young: false,
328                        user_plan: None,
329                        tab_index: Some(0),
330                    }
331                    .into_any_element(),
332                ))
333                .child(example_group_with_title(
334                    "Signed In States",
335                    vec![
336                        single_example(
337                            "Free Plan",
338                            AiUpsellCard {
339                                sign_in_status: SignInStatus::SignedIn,
340                                sign_in: Arc::new(|_, _| {}),
341                                account_too_young: false,
342                                user_plan: Some(Plan::ZedFree),
343                                tab_index: Some(1),
344                            }
345                            .into_any_element(),
346                        ),
347                        single_example(
348                            "Free Plan but Young Account",
349                            AiUpsellCard {
350                                sign_in_status: SignInStatus::SignedIn,
351                                sign_in: Arc::new(|_, _| {}),
352                                account_too_young: true,
353                                user_plan: Some(Plan::ZedFree),
354                                tab_index: Some(1),
355                            }
356                            .into_any_element(),
357                        ),
358                        single_example(
359                            "Pro Trial",
360                            AiUpsellCard {
361                                sign_in_status: SignInStatus::SignedIn,
362                                sign_in: Arc::new(|_, _| {}),
363                                account_too_young: false,
364                                user_plan: Some(Plan::ZedProTrial),
365                                tab_index: Some(1),
366                            }
367                            .into_any_element(),
368                        ),
369                        single_example(
370                            "Pro Plan",
371                            AiUpsellCard {
372                                sign_in_status: SignInStatus::SignedIn,
373                                sign_in: Arc::new(|_, _| {}),
374                                account_too_young: false,
375                                user_plan: Some(Plan::ZedPro),
376                                tab_index: Some(1),
377                            }
378                            .into_any_element(),
379                        ),
380                        single_example(
381                            "Business Plan",
382                            AiUpsellCard {
383                                sign_in_status: SignInStatus::SignedIn,
384                                sign_in: Arc::new(|_, _| {}),
385                                account_too_young: false,
386                                user_plan: Some(Plan::ZedBusiness),
387                                tab_index: Some(1),
388                            }
389                            .into_any_element(),
390                        ),
391                        single_example(
392                            "Student Plan",
393                            AiUpsellCard {
394                                sign_in_status: SignInStatus::SignedIn,
395                                sign_in: Arc::new(|_, _| {}),
396                                account_too_young: false,
397                                user_plan: Some(Plan::ZedStudent),
398                                tab_index: Some(1),
399                            }
400                            .into_any_element(),
401                        ),
402                    ],
403                ))
404                .into_any_element(),
405        )
406    }
407}