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 sign_in_status: SignInStatus,
16 sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
17 account_too_young: bool,
18 user_plan: Option<Plan>,
19 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 pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
48 self.tab_index = tab_index;
49 self
50 }
51}
52
53impl RenderOnce for AiUpsellCard {
54 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
55 let plan_definitions = PlanDefinitions;
56 let young_account_banner = YoungAccountBanner;
57
58 let pro_section = v_flex()
59 .flex_grow()
60 .w_full()
61 .gap_1()
62 .child(
63 h_flex()
64 .gap_2()
65 .child(
66 Label::new("Pro")
67 .size(LabelSize::Small)
68 .color(Color::Accent)
69 .buffer_font(cx),
70 )
71 .child(Divider::horizontal()),
72 )
73 .child(plan_definitions.pro_plan(false));
74
75 let free_section = v_flex()
76 .flex_grow()
77 .w_full()
78 .gap_1()
79 .child(
80 h_flex()
81 .gap_2()
82 .child(
83 Label::new("Free")
84 .size(LabelSize::Small)
85 .color(Color::Muted)
86 .buffer_font(cx),
87 )
88 .child(Divider::horizontal()),
89 )
90 .child(plan_definitions.free_plan());
91
92 let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
93 Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
94 .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
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_animation(
151 "loading_stamp",
152 Animation::new(Duration::from_secs(10)).repeat(),
153 |this, delta| this.transform(Transformation::rotate(percentage(delta))),
154 ),
155 );
156
157 let pro_trial_stamp = div()
158 .absolute()
159 .top_2()
160 .right_2()
161 .size(rems_from_px(72.))
162 .child(
163 Vector::new(
164 VectorName::ProTrialStamp,
165 rems_from_px(72.),
166 rems_from_px(72.),
167 )
168 .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
169 );
170
171 match self.sign_in_status {
172 SignInStatus::SignedIn => match self.user_plan {
173 None | Some(Plan::ZedFree) => card
174 .child(Label::new("Try Zed AI").size(LabelSize::Large))
175 .map(|this| {
176 if self.account_too_young {
177 this.child(young_account_banner).child(
178 v_flex()
179 .mt_2()
180 .gap_1()
181 .child(
182 h_flex()
183 .gap_2()
184 .child(
185 Label::new("Pro")
186 .size(LabelSize::Small)
187 .color(Color::Accent)
188 .buffer_font(cx),
189 )
190 .child(Divider::horizontal()),
191 )
192 .child(plan_definitions.pro_plan(true))
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::ZedProTrial) => 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(plan_definitions.pro_trial(false)),
248 Some(Plan::ZedPro) => 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(plan_definitions.pro_plan(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}