1use std::sync::Arc;
2
3use client::{Client, UserStore, zed_urls};
4use cloud_llm_client::{Plan, PlanV1, 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 is_v2_plan = self.user_plan.map_or(true, |plan| plan.is_v2());
53
54 let pro_section = v_flex()
55 .flex_grow()
56 .w_full()
57 .gap_1()
58 .child(
59 h_flex()
60 .gap_2()
61 .child(
62 Label::new("Pro")
63 .size(LabelSize::Small)
64 .color(Color::Accent)
65 .buffer_font(cx),
66 )
67 .child(Divider::horizontal()),
68 )
69 .child(PlanDefinitions.pro_plan(is_v2_plan, false));
70
71 let free_section = v_flex()
72 .flex_grow()
73 .w_full()
74 .gap_1()
75 .child(
76 h_flex()
77 .gap_2()
78 .child(
79 Label::new("Free")
80 .size(LabelSize::Small)
81 .color(Color::Muted)
82 .buffer_font(cx),
83 )
84 .child(Divider::horizontal()),
85 )
86 .child(PlanDefinitions.free_plan(is_v2_plan));
87
88 let grid_bg = h_flex()
89 .absolute()
90 .inset_0()
91 .w_full()
92 .h(px(240.))
93 .bg(gpui::pattern_slash(
94 cx.theme().colors().border.opacity(0.1),
95 2.,
96 25.,
97 ));
98
99 let gradient_bg = div()
100 .absolute()
101 .inset_0()
102 .size_full()
103 .bg(gpui::linear_gradient(
104 180.,
105 gpui::linear_color_stop(
106 cx.theme().colors().elevated_surface_background.opacity(0.8),
107 0.,
108 ),
109 gpui::linear_color_stop(
110 cx.theme().colors().elevated_surface_background.opacity(0.),
111 0.8,
112 ),
113 ));
114
115 let description = PlanDefinitions::AI_DESCRIPTION;
116
117 let card = v_flex()
118 .relative()
119 .flex_grow()
120 .p_4()
121 .pt_3()
122 .border_1()
123 .border_color(cx.theme().colors().border)
124 .rounded_lg()
125 .overflow_hidden()
126 .child(grid_bg)
127 .child(gradient_bg);
128
129 let plans_section = h_flex()
130 .w_full()
131 .mt_1p5()
132 .mb_2p5()
133 .items_start()
134 .gap_6()
135 .child(free_section)
136 .child(pro_section);
137
138 let footer_container = v_flex().items_center().gap_1();
139
140 let certified_user_stamp = div()
141 .absolute()
142 .top_2()
143 .right_2()
144 .size(rems_from_px(72.))
145 .child(
146 Vector::new(
147 VectorName::ProUserStamp,
148 rems_from_px(72.),
149 rems_from_px(72.),
150 )
151 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
152 .with_rotate_animation(10),
153 );
154
155 let pro_trial_stamp = div()
156 .absolute()
157 .top_2()
158 .right_2()
159 .size(rems_from_px(72.))
160 .child(
161 Vector::new(
162 VectorName::ProTrialStamp,
163 rems_from_px(72.),
164 rems_from_px(72.),
165 )
166 .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
167 );
168
169 match self.sign_in_status {
170 SignInStatus::SignedIn => match self.user_plan {
171 None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => card
172 .child(Label::new("Try Zed AI").size(LabelSize::Large))
173 .map(|this| {
174 if self.account_too_young {
175 this.child(YoungAccountBanner).child(
176 v_flex()
177 .mt_2()
178 .gap_1()
179 .child(
180 h_flex()
181 .gap_2()
182 .child(
183 Label::new("Pro")
184 .size(LabelSize::Small)
185 .color(Color::Accent)
186 .buffer_font(cx),
187 )
188 .child(Divider::horizontal()),
189 )
190 .child(PlanDefinitions.pro_plan(is_v2_plan, true))
191 .child(
192 Button::new("pro", "Get Started")
193 .full_width()
194 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
195 .on_click(move |_, _window, cx| {
196 telemetry::event!(
197 "Upgrade To Pro Clicked",
198 state = "young-account"
199 );
200 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
201 }),
202 ),
203 )
204 } else {
205 this.child(
206 div()
207 .max_w_3_4()
208 .mb_2()
209 .child(Label::new(description).color(Color::Muted)),
210 )
211 .child(plans_section)
212 .child(
213 footer_container
214 .child(
215 Button::new("start_trial", "Start Pro Trial")
216 .full_width()
217 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
218 .when_some(self.tab_index, |this, tab_index| {
219 this.tab_index(tab_index)
220 })
221 .on_click(move |_, _window, cx| {
222 telemetry::event!(
223 "Start Trial Clicked",
224 state = "post-sign-in"
225 );
226 cx.open_url(&zed_urls::start_trial_url(cx))
227 }),
228 )
229 .child(
230 Label::new("14 days, no credit card required")
231 .size(LabelSize::Small)
232 .color(Color::Muted),
233 ),
234 )
235 }
236 }),
237 Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
238 card.child(pro_trial_stamp)
239 .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
240 .child(
241 Label::new("Here's what you get for the next 14 days:")
242 .color(Color::Muted)
243 .mb_2(),
244 )
245 .child(PlanDefinitions.pro_trial(plan.is_v2(), false))
246 }
247 Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::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(PlanDefinitions.pro_plan(plan.is_v2(), 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::V2(PlanV2::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::V2(PlanV2::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::V2(PlanV2::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::V2(PlanV2::ZedPro)),
361 tab_index: Some(1),
362 }
363 .into_any_element(),
364 ),
365 ],
366 ))
367 .into_any_element(),
368 )
369 }
370}