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")
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("start_trial", "Start 14-day Free Pro Trial")
213 .full_width()
214 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
215 .when_some(self.tab_index, |this, tab_index| {
216 this.tab_index(tab_index)
217 })
218 .on_click(move |_, _window, cx| {
219 telemetry::event!(
220 "Start Trial Clicked",
221 state = "post-sign-in"
222 );
223 cx.open_url(&zed_urls::start_trial_url(cx))
224 }),
225 )
226 .child(
227 Label::new("No credit card required")
228 .size(LabelSize::Small)
229 .color(Color::Muted),
230 ),
231 )
232 }
233 }),
234 Some(Plan::ZedProTrial) => card
235 .child(pro_trial_stamp)
236 .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
237 .child(
238 Label::new("Here's what you get for the next 14 days:")
239 .color(Color::Muted)
240 .mb_2(),
241 )
242 .child(plan_definitions.pro_trial(false)),
243 Some(Plan::ZedPro) => card
244 .child(certified_user_stamp)
245 .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
246 .child(
247 Label::new("Here's what you get:")
248 .color(Color::Muted)
249 .mb_2(),
250 )
251 .child(plan_definitions.pro_plan(false)),
252 },
253 // Signed Out State
254 _ => card
255 .child(Label::new("Try Zed AI").size(LabelSize::Large))
256 .child(
257 div()
258 .max_w_3_4()
259 .mb_2()
260 .child(Label::new(description).color(Color::Muted)),
261 )
262 .child(plans_section)
263 .child(
264 Button::new("sign_in", "Sign In")
265 .full_width()
266 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
267 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
268 .on_click({
269 let callback = self.sign_in.clone();
270 move |_, window, cx| {
271 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
272 callback(window, cx)
273 }
274 }),
275 ),
276 }
277 }
278}
279
280impl Component for AiUpsellCard {
281 fn scope() -> ComponentScope {
282 ComponentScope::Onboarding
283 }
284
285 fn name() -> &'static str {
286 "AI Upsell Card"
287 }
288
289 fn sort_name() -> &'static str {
290 "AI Upsell Card"
291 }
292
293 fn description() -> Option<&'static str> {
294 Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
295 }
296
297 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
298 Some(
299 v_flex()
300 .gap_4()
301 .items_center()
302 .max_w_4_5()
303 .child(single_example(
304 "Signed Out State",
305 AiUpsellCard {
306 sign_in_status: SignInStatus::SignedOut,
307 sign_in: Arc::new(|_, _| {}),
308 account_too_young: false,
309 user_plan: None,
310 tab_index: Some(0),
311 }
312 .into_any_element(),
313 ))
314 .child(example_group_with_title(
315 "Signed In States",
316 vec![
317 single_example(
318 "Free Plan",
319 AiUpsellCard {
320 sign_in_status: SignInStatus::SignedIn,
321 sign_in: Arc::new(|_, _| {}),
322 account_too_young: false,
323 user_plan: Some(Plan::ZedFree),
324 tab_index: Some(1),
325 }
326 .into_any_element(),
327 ),
328 single_example(
329 "Free Plan but Young Account",
330 AiUpsellCard {
331 sign_in_status: SignInStatus::SignedIn,
332 sign_in: Arc::new(|_, _| {}),
333 account_too_young: true,
334 user_plan: Some(Plan::ZedFree),
335 tab_index: Some(1),
336 }
337 .into_any_element(),
338 ),
339 single_example(
340 "Pro Trial",
341 AiUpsellCard {
342 sign_in_status: SignInStatus::SignedIn,
343 sign_in: Arc::new(|_, _| {}),
344 account_too_young: false,
345 user_plan: Some(Plan::ZedProTrial),
346 tab_index: Some(1),
347 }
348 .into_any_element(),
349 ),
350 single_example(
351 "Pro Plan",
352 AiUpsellCard {
353 sign_in_status: SignInStatus::SignedIn,
354 sign_in: Arc::new(|_, _| {}),
355 account_too_young: false,
356 user_plan: Some(Plan::ZedPro),
357 tab_index: Some(1),
358 }
359 .into_any_element(),
360 ),
361 ],
362 ))
363 .into_any_element(),
364 )
365 }
366}