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