ai_upsell_card.rs

  1use std::sync::Arc;
  2
  3use client::{Client, UserStore, zed_urls};
  4use cloud_llm_client::Plan;
  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 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(PlanDefinitions.pro_plan(cx.has_flag::<BillingV2FeatureFlag>(), 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(PlanDefinitions.free_plan(cx.has_flag::<BillingV2FeatureFlag>()));
 86
 87        let grid_bg = h_flex()
 88            .absolute()
 89            .inset_0()
 90            .w_full()
 91            .h(px(240.))
 92            .bg(gpui::pattern_slash(
 93                cx.theme().colors().border.opacity(0.1),
 94                2.,
 95                25.,
 96            ));
 97
 98        let gradient_bg = div()
 99            .absolute()
100            .inset_0()
101            .size_full()
102            .bg(gpui::linear_gradient(
103                180.,
104                gpui::linear_color_stop(
105                    cx.theme().colors().elevated_surface_background.opacity(0.8),
106                    0.,
107                ),
108                gpui::linear_color_stop(
109                    cx.theme().colors().elevated_surface_background.opacity(0.),
110                    0.8,
111                ),
112            ));
113
114        let description = PlanDefinitions::AI_DESCRIPTION;
115
116        let card = v_flex()
117            .relative()
118            .flex_grow()
119            .p_4()
120            .pt_3()
121            .border_1()
122            .border_color(cx.theme().colors().border)
123            .rounded_lg()
124            .overflow_hidden()
125            .child(grid_bg)
126            .child(gradient_bg);
127
128        let plans_section = h_flex()
129            .w_full()
130            .mt_1p5()
131            .mb_2p5()
132            .items_start()
133            .gap_6()
134            .child(free_section)
135            .child(pro_section);
136
137        let footer_container = v_flex().items_center().gap_1();
138
139        let certified_user_stamp = div()
140            .absolute()
141            .top_2()
142            .right_2()
143            .size(rems_from_px(72.))
144            .child(
145                Vector::new(
146                    VectorName::ProUserStamp,
147                    rems_from_px(72.),
148                    rems_from_px(72.),
149                )
150                .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
151                .with_rotate_animation(10),
152            );
153
154        let pro_trial_stamp = div()
155            .absolute()
156            .top_2()
157            .right_2()
158            .size(rems_from_px(72.))
159            .child(
160                Vector::new(
161                    VectorName::ProTrialStamp,
162                    rems_from_px(72.),
163                    rems_from_px(72.),
164                )
165                .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
166            );
167
168        match self.sign_in_status {
169            SignInStatus::SignedIn => match self.user_plan {
170                None | Some(Plan::ZedFree) => card
171                    .child(Label::new("Try Zed AI").size(LabelSize::Large))
172                    .map(|this| {
173                        if self.account_too_young {
174                            this.child(YoungAccountBanner).child(
175                                v_flex()
176                                    .mt_2()
177                                    .gap_1()
178                                    .child(
179                                        h_flex()
180                                            .gap_2()
181                                            .child(
182                                                Label::new("Pro")
183                                                    .size(LabelSize::Small)
184                                                    .color(Color::Accent)
185                                                    .buffer_font(cx),
186                                            )
187                                            .child(Divider::horizontal()),
188                                    )
189                                    .child(
190                                        PlanDefinitions
191                                            .pro_plan(cx.has_flag::<BillingV2FeatureFlag>(), true),
192                                    )
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 @ Plan::ZedProTrial | plan @ Plan::ZedProTrialV2) => 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(PlanDefinitions.pro_trial(plan == Plan::ZedProTrialV2, false)),
248                Some(plan @ Plan::ZedPro | plan @ Plan::ZedProV2) => 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(PlanDefinitions.pro_plan(plan == Plan::ZedProV2, 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}