zeta: Update onboarding modal with subscription info (#30439)

Marshall Bowers created

This PR updates the edit prediction onboarding modal with steps about
subscribing to a plan.

When the user is not subscribed to a plan, we display a link to the
account page to sign up for one:

<img width="612" alt="Screenshot 2025-05-09 at 6 04 05 PM"
src="https://github.com/user-attachments/assets/0300194a-c419-43d9-8214-080674d31e12"
/>

If the user is already subscribed to a plan we indicate which plan they
are on and how many edit predictions they get with it:

<img width="616" alt="Screenshot 2025-05-09 at 6 03 16 PM"
src="https://github.com/user-attachments/assets/e2506096-e499-41f2-ba1f-fca768cb48b9"
/>

<img width="595" alt="Screenshot 2025-05-09 at 5 46 18 PM"
src="https://github.com/user-attachments/assets/de82f8c2-cad8-45fb-8988-26606a8dc3e1"
/>

Release Notes:

- N/A

Change summary

Cargo.lock                                                      |  1 
crates/inline_completion_button/src/inline_completion_button.rs | 10 
crates/zeta/Cargo.toml                                          |  1 
crates/zeta/src/onboarding_modal.rs                             | 49 ++
4 files changed, 57 insertions(+), 4 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -18952,6 +18952,7 @@ dependencies = [
  "paths",
  "postage",
  "project",
+ "proto",
  "regex",
  "release_channel",
  "reqwest_client",

crates/inline_completion_button/src/inline_completion_button.rs 🔗

@@ -237,11 +237,17 @@ impl Render for InlineCompletionButton {
 
                 let current_user_terms_accepted =
                     self.user_store.read(cx).current_user_has_accepted_terms();
+                let has_subscription = self.user_store.read(cx).current_plan().is_some()
+                    && self.user_store.read(cx).subscription_period().is_some();
 
-                if !current_user_terms_accepted.unwrap_or(false) {
+                if !has_subscription || !current_user_terms_accepted.unwrap_or(false) {
                     let signed_in = current_user_terms_accepted.is_some();
                     let tooltip_meta = if signed_in {
-                        "Read Terms of Service"
+                        if has_subscription {
+                            "Read Terms of Service"
+                        } else {
+                            "Choose a Plan"
+                        }
                     } else {
                         "Sign in to use"
                     };

crates/zeta/Cargo.toml 🔗

@@ -39,6 +39,7 @@ migrator.workspace = true
 paths.workspace = true
 postage.workspace = true
 project.workspace = true
+proto.workspace = true
 regex.workspace = true
 release_channel.workspace = true
 serde.workspace = true

crates/zeta/src/onboarding_modal.rs 🔗

@@ -2,7 +2,7 @@ use std::{sync::Arc, time::Duration};
 
 use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event};
 use anyhow::Context as _;
-use client::{Client, UserStore};
+use client::{Client, UserStore, zed_urls};
 use db::kvp::KEY_VALUE_STORE;
 use fs::Fs;
 use gpui::{
@@ -246,6 +246,12 @@ impl Render for ZedPredictModal {
         let window_height = window.viewport_size().height;
         let max_height = window_height - px(200.);
 
+        let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
+        let plan = self.user_store.read(cx).current_plan().filter(|_| {
+            // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
+            has_subscription_period
+        });
+
         let base = v_flex()
             .id("edit-prediction-onboarding")
             .key_context("ZedPredictModal")
@@ -377,6 +383,45 @@ impl Render for ZedPredictModal {
             };
 
             base.child(Label::new(copy).color(Color::Muted))
+                .child(h_flex().map(|parent| {
+                    if let Some(plan) = plan {
+                        parent.child(
+                            Checkbox::new("plan", ToggleState::Selected)
+                                .fill()
+                                .disabled(true)
+                                .label(format!(
+                                    "You get {} edit predictions through your {}.",
+                                    if plan == proto::Plan::Free {
+                                        "2,000"
+                                    } else {
+                                        "unlimited"
+                                    },
+                                    match plan {
+                                        proto::Plan::Free => "Zed Free plan",
+                                        proto::Plan::ZedPro => "Zed Pro plan",
+                                        proto::Plan::ZedProTrial => "Zed Pro trial",
+                                    }
+                                )),
+                        )
+                    } else {
+                        parent
+                            .child(
+                                Checkbox::new("plan-required", ToggleState::Unselected)
+                                    .fill()
+                                    .disabled(true)
+                                    .label("To get started with edit prediction"),
+                            )
+                            .child(
+                                Button::new("subscribe", "choose a plan")
+                                    .icon(IconName::ArrowUpRight)
+                                    .icon_size(IconSize::Indicator)
+                                    .icon_color(Color::Muted)
+                                    .on_click(|_event, _window, cx| {
+                                        cx.open_url(&zed_urls::account_url(cx));
+                                    }),
+                            )
+                    }
+                }))
                 .child(
                     h_flex()
                         .child(
@@ -447,7 +492,7 @@ impl Render for ZedPredictModal {
                         .w_full()
                         .child(
                             Button::new("accept-tos", "Enable Edit Prediction")
-                                .disabled(!self.terms_of_service)
+                                .disabled(plan.is_none() || !self.terms_of_service)
                                 .style(ButtonStyle::Tinted(TintColor::Accent))
                                 .full_width()
                                 .on_click(cx.listener(Self::accept_and_enable)),