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