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::ZedStudent) => card
254 .child(certified_user_stamp)
255 .child(Label::new("You're in the Zed Student 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.student_plan()),
262 },
263 // Signed Out State
264 _ => card
265 .child(Label::new("Try Zed AI").size(LabelSize::Large))
266 .child(
267 div()
268 .max_w_3_4()
269 .mb_2()
270 .child(Label::new(description).color(Color::Muted)),
271 )
272 .child(plans_section)
273 .child(
274 Button::new("sign_in", "Sign In")
275 .full_width()
276 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
277 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
278 .on_click({
279 let callback = self.sign_in.clone();
280 move |_, window, cx| {
281 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
282 callback(window, cx)
283 }
284 }),
285 ),
286 }
287 }
288}
289
290impl Component for AiUpsellCard {
291 fn scope() -> ComponentScope {
292 ComponentScope::Onboarding
293 }
294
295 fn name() -> &'static str {
296 "AI Upsell Card"
297 }
298
299 fn sort_name() -> &'static str {
300 "AI Upsell Card"
301 }
302
303 fn description() -> Option<&'static str> {
304 Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
305 }
306
307 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
308 Some(
309 v_flex()
310 .gap_4()
311 .items_center()
312 .max_w_4_5()
313 .child(single_example(
314 "Signed Out State",
315 AiUpsellCard {
316 sign_in_status: SignInStatus::SignedOut,
317 sign_in: Arc::new(|_, _| {}),
318 account_too_young: false,
319 user_plan: None,
320 tab_index: Some(0),
321 }
322 .into_any_element(),
323 ))
324 .child(example_group_with_title(
325 "Signed In States",
326 vec![
327 single_example(
328 "Free Plan",
329 AiUpsellCard {
330 sign_in_status: SignInStatus::SignedIn,
331 sign_in: Arc::new(|_, _| {}),
332 account_too_young: false,
333 user_plan: Some(Plan::ZedFree),
334 tab_index: Some(1),
335 }
336 .into_any_element(),
337 ),
338 single_example(
339 "Free Plan but Young Account",
340 AiUpsellCard {
341 sign_in_status: SignInStatus::SignedIn,
342 sign_in: Arc::new(|_, _| {}),
343 account_too_young: true,
344 user_plan: Some(Plan::ZedFree),
345 tab_index: Some(1),
346 }
347 .into_any_element(),
348 ),
349 single_example(
350 "Pro Trial",
351 AiUpsellCard {
352 sign_in_status: SignInStatus::SignedIn,
353 sign_in: Arc::new(|_, _| {}),
354 account_too_young: false,
355 user_plan: Some(Plan::ZedProTrial),
356 tab_index: Some(1),
357 }
358 .into_any_element(),
359 ),
360 single_example(
361 "Pro Plan",
362 AiUpsellCard {
363 sign_in_status: SignInStatus::SignedIn,
364 sign_in: Arc::new(|_, _| {}),
365 account_too_young: false,
366 user_plan: Some(Plan::ZedPro),
367 tab_index: Some(1),
368 }
369 .into_any_element(),
370 ),
371 ],
372 ))
373 .into_any_element(),
374 )
375 }
376}