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