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