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