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