ai_upsell_card.rs

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