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