1use std::sync::Arc;
2
3use client::{Client, UserStore, zed_urls};
4use cloud_llm_client::Plan;
5use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt};
6use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
7use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
8
9use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions};
10
11#[derive(IntoElement, RegisterComponent)]
12pub struct AiUpsellCard {
13 sign_in_status: SignInStatus,
14 sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
15 account_too_young: bool,
16 user_plan: Option<Plan>,
17 tab_index: Option<isize>,
18}
19
20impl AiUpsellCard {
21 pub fn new(
22 client: Arc<Client>,
23 user_store: &Entity<UserStore>,
24 user_plan: Option<Plan>,
25 cx: &mut App,
26 ) -> Self {
27 let status = *client.status().borrow();
28 let store = user_store.read(cx);
29
30 Self {
31 user_plan,
32 sign_in_status: status.into(),
33 sign_in: Arc::new(move |_window, cx| {
34 cx.spawn({
35 let client = client.clone();
36 async move |cx| client.sign_in_with_optional_connect(true, cx).await
37 })
38 .detach_and_log_err(cx);
39 }),
40 account_too_young: store.account_too_young(),
41 tab_index: None,
42 }
43 }
44
45 pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
46 self.tab_index = tab_index;
47 self
48 }
49}
50
51impl RenderOnce for AiUpsellCard {
52 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
53 let is_v2_plan = self
54 .user_plan
55 .map_or(cx.has_flag::<BillingV2FeatureFlag>(), |plan| plan.is_v2());
56
57 let pro_section = v_flex()
58 .flex_grow()
59 .w_full()
60 .gap_1()
61 .child(
62 h_flex()
63 .gap_2()
64 .child(
65 Label::new("Pro")
66 .size(LabelSize::Small)
67 .color(Color::Accent)
68 .buffer_font(cx),
69 )
70 .child(Divider::horizontal()),
71 )
72 .child(PlanDefinitions.pro_plan(is_v2_plan, false));
73
74 let free_section = v_flex()
75 .flex_grow()
76 .w_full()
77 .gap_1()
78 .child(
79 h_flex()
80 .gap_2()
81 .child(
82 Label::new("Free")
83 .size(LabelSize::Small)
84 .color(Color::Muted)
85 .buffer_font(cx),
86 )
87 .child(Divider::horizontal()),
88 )
89 .child(PlanDefinitions.free_plan(is_v2_plan));
90
91 let grid_bg = h_flex()
92 .absolute()
93 .inset_0()
94 .w_full()
95 .h(px(240.))
96 .bg(gpui::pattern_slash(
97 cx.theme().colors().border.opacity(0.1),
98 2.,
99 25.,
100 ));
101
102 let gradient_bg = div()
103 .absolute()
104 .inset_0()
105 .size_full()
106 .bg(gpui::linear_gradient(
107 180.,
108 gpui::linear_color_stop(
109 cx.theme().colors().elevated_surface_background.opacity(0.8),
110 0.,
111 ),
112 gpui::linear_color_stop(
113 cx.theme().colors().elevated_surface_background.opacity(0.),
114 0.8,
115 ),
116 ));
117
118 let description = PlanDefinitions::AI_DESCRIPTION;
119
120 let card = v_flex()
121 .relative()
122 .flex_grow()
123 .p_4()
124 .pt_3()
125 .border_1()
126 .border_color(cx.theme().colors().border)
127 .rounded_lg()
128 .overflow_hidden()
129 .child(grid_bg)
130 .child(gradient_bg);
131
132 let plans_section = h_flex()
133 .w_full()
134 .mt_1p5()
135 .mb_2p5()
136 .items_start()
137 .gap_6()
138 .child(free_section)
139 .child(pro_section);
140
141 let footer_container = v_flex().items_center().gap_1();
142
143 let certified_user_stamp = div()
144 .absolute()
145 .top_2()
146 .right_2()
147 .size(rems_from_px(72.))
148 .child(
149 Vector::new(
150 VectorName::ProUserStamp,
151 rems_from_px(72.),
152 rems_from_px(72.),
153 )
154 .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
155 .with_rotate_animation(10),
156 );
157
158 let pro_trial_stamp = div()
159 .absolute()
160 .top_2()
161 .right_2()
162 .size(rems_from_px(72.))
163 .child(
164 Vector::new(
165 VectorName::ProTrialStamp,
166 rems_from_px(72.),
167 rems_from_px(72.),
168 )
169 .color(Color::Custom(cx.theme().colors().text.alpha(0.2))),
170 );
171
172 match self.sign_in_status {
173 SignInStatus::SignedIn => match self.user_plan {
174 None | Some(Plan::ZedFree | Plan::ZedFreeV2) => card
175 .child(Label::new("Try Zed AI").size(LabelSize::Large))
176 .map(|this| {
177 if self.account_too_young {
178 this.child(YoungAccountBanner).child(
179 v_flex()
180 .mt_2()
181 .gap_1()
182 .child(
183 h_flex()
184 .gap_2()
185 .child(
186 Label::new("Pro")
187 .size(LabelSize::Small)
188 .color(Color::Accent)
189 .buffer_font(cx),
190 )
191 .child(Divider::horizontal()),
192 )
193 .child(PlanDefinitions.pro_plan(is_v2_plan, true))
194 .child(
195 Button::new("pro", "Get Started")
196 .full_width()
197 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
198 .on_click(move |_, _window, cx| {
199 telemetry::event!(
200 "Upgrade To Pro Clicked",
201 state = "young-account"
202 );
203 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
204 }),
205 ),
206 )
207 } else {
208 this.child(
209 div()
210 .max_w_3_4()
211 .mb_2()
212 .child(Label::new(description).color(Color::Muted)),
213 )
214 .child(plans_section)
215 .child(
216 footer_container
217 .child(
218 Button::new("start_trial", "Start 14-day Free Pro Trial")
219 .full_width()
220 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
221 .when_some(self.tab_index, |this, tab_index| {
222 this.tab_index(tab_index)
223 })
224 .on_click(move |_, _window, cx| {
225 telemetry::event!(
226 "Start Trial Clicked",
227 state = "post-sign-in"
228 );
229 cx.open_url(&zed_urls::start_trial_url(cx))
230 }),
231 )
232 .child(
233 Label::new("No credit card required")
234 .size(LabelSize::Small)
235 .color(Color::Muted),
236 ),
237 )
238 }
239 }),
240 Some(plan @ (Plan::ZedProTrial | Plan::ZedProTrialV2)) => card
241 .child(pro_trial_stamp)
242 .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
243 .child(
244 Label::new("Here's what you get for the next 14 days:")
245 .color(Color::Muted)
246 .mb_2(),
247 )
248 .child(PlanDefinitions.pro_trial(plan.is_v2(), false)),
249 Some(plan @ (Plan::ZedPro | Plan::ZedProV2)) => card
250 .child(certified_user_stamp)
251 .child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
252 .child(
253 Label::new("Here's what you get:")
254 .color(Color::Muted)
255 .mb_2(),
256 )
257 .child(PlanDefinitions.pro_plan(plan.is_v2(), false)),
258 },
259 // Signed Out State
260 _ => card
261 .child(Label::new("Try Zed AI").size(LabelSize::Large))
262 .child(
263 div()
264 .max_w_3_4()
265 .mb_2()
266 .child(Label::new(description).color(Color::Muted)),
267 )
268 .child(plans_section)
269 .child(
270 Button::new("sign_in", "Sign In")
271 .full_width()
272 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
273 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
274 .on_click({
275 let callback = self.sign_in.clone();
276 move |_, window, cx| {
277 telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
278 callback(window, cx)
279 }
280 }),
281 ),
282 }
283 }
284}
285
286impl Component for AiUpsellCard {
287 fn scope() -> ComponentScope {
288 ComponentScope::Onboarding
289 }
290
291 fn name() -> &'static str {
292 "AI Upsell Card"
293 }
294
295 fn sort_name() -> &'static str {
296 "AI Upsell Card"
297 }
298
299 fn description() -> Option<&'static str> {
300 Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
301 }
302
303 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
304 Some(
305 v_flex()
306 .gap_4()
307 .items_center()
308 .max_w_4_5()
309 .child(single_example(
310 "Signed Out State",
311 AiUpsellCard {
312 sign_in_status: SignInStatus::SignedOut,
313 sign_in: Arc::new(|_, _| {}),
314 account_too_young: false,
315 user_plan: None,
316 tab_index: Some(0),
317 }
318 .into_any_element(),
319 ))
320 .child(example_group_with_title(
321 "Signed In States",
322 vec![
323 single_example(
324 "Free Plan",
325 AiUpsellCard {
326 sign_in_status: SignInStatus::SignedIn,
327 sign_in: Arc::new(|_, _| {}),
328 account_too_young: false,
329 user_plan: Some(Plan::ZedFree),
330 tab_index: Some(1),
331 }
332 .into_any_element(),
333 ),
334 single_example(
335 "Free Plan but Young Account",
336 AiUpsellCard {
337 sign_in_status: SignInStatus::SignedIn,
338 sign_in: Arc::new(|_, _| {}),
339 account_too_young: true,
340 user_plan: Some(Plan::ZedFree),
341 tab_index: Some(1),
342 }
343 .into_any_element(),
344 ),
345 single_example(
346 "Pro Trial",
347 AiUpsellCard {
348 sign_in_status: SignInStatus::SignedIn,
349 sign_in: Arc::new(|_, _| {}),
350 account_too_young: false,
351 user_plan: Some(Plan::ZedProTrial),
352 tab_index: Some(1),
353 }
354 .into_any_element(),
355 ),
356 single_example(
357 "Pro Plan",
358 AiUpsellCard {
359 sign_in_status: SignInStatus::SignedIn,
360 sign_in: Arc::new(|_, _| {}),
361 account_too_young: false,
362 user_plan: Some(Plan::ZedPro),
363 tab_index: Some(1),
364 }
365 .into_any_element(),
366 ),
367 ],
368 ))
369 .into_any_element(),
370 )
371 }
372}