diff --git a/Cargo.lock b/Cargo.lock index 392be06d6c..71ec98a7bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11186,6 +11186,7 @@ dependencies = [ "dev_server_projects", "editor", "extensions_ui", + "feature_flags", "feedback", "gpui", "http_client", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index cd5bed6828..6464ffb0cb 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -92,6 +92,7 @@ pub struct UserStore { by_github_login: HashMap, participant_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, + current_plan: Option, current_user: watch::Receiver>>, contacts: Vec>, incoming_contact_requests: Vec>, @@ -139,6 +140,7 @@ impl UserStore { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ + client.add_message_handler(cx.weak_model(), Self::handle_update_plan), client.add_message_handler(cx.weak_model(), Self::handle_update_contacts), client.add_message_handler(cx.weak_model(), Self::handle_update_invite_info), client.add_message_handler(cx.weak_model(), Self::handle_show_contacts), @@ -147,6 +149,7 @@ impl UserStore { users: Default::default(), by_github_login: Default::default(), current_user: current_user_rx, + current_plan: None, contacts: Default::default(), incoming_contact_requests: Default::default(), participant_indices: Default::default(), @@ -280,6 +283,18 @@ impl UserStore { Ok(()) } + async fn handle_update_plan( + this: Model, + message: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.current_plan = Some(message.payload.plan()); + cx.notify(); + })?; + Ok(()) + } + fn update_contacts( &mut self, message: UpdateContacts, @@ -657,6 +672,10 @@ impl UserStore { self.current_user.borrow().clone() } + pub fn current_plan(&self) -> Option { + self.current_plan + } + pub fn watch_current_user(&self) -> watch::Receiver>> { self.current_user.clone() } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 25e8f9edf8..81ce57561a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1137,6 +1137,8 @@ impl Server { .await?; } + update_user_plan(user.id, session).await?; + let (contacts, dev_server_projects) = future::try_join( self.app_state.db.get_contacts(user.id), self.app_state.db.dev_server_projects_update(user.id), @@ -3535,6 +3537,27 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool { version.0.minor() < 139 } +async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> { + let db = session.db().await; + let active_subscriptions = db.get_active_billing_subscriptions(user_id).await?; + + let plan = if session.is_staff() || !active_subscriptions.is_empty() { + proto::Plan::ZedPro + } else { + proto::Plan::Free + }; + + session + .peer + .send( + session.connection_id, + proto::UpdateUserPlan { plan: plan.into() }, + ) + .trace_err(); + + Ok(()) +} + async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> { subscribe_user_to_channels( session.user_id().ok_or_else(|| anyhow!("must be a user"))?, diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index cb37ca392e..c37ae698d0 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -48,6 +48,11 @@ impl FeatureFlag for GroupedDiagnostics { const NAME: &'static str = "grouped-diagnostics"; } +pub struct ZedPro {} +impl FeatureFlag for ZedPro { + const NAME: &'static str = "zed-pro"; +} + pub trait FeatureFlagViewExt { fn observe_flag(&mut self, callback: F) -> Subscription where diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 2c70bcc613..e6cc7c74a2 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -126,6 +126,7 @@ message Envelope { Unfollow unfollow = 101; GetPrivateUserInfo get_private_user_info = 102; GetPrivateUserInfoResponse get_private_user_info_response = 103; + UpdateUserPlan update_user_plan = 234; // current max UpdateDiffBase update_diff_base = 104; OnTypeFormatting on_type_formatting = 105; @@ -256,7 +257,7 @@ message Envelope { OpenContext open_context = 212; OpenContextResponse open_context_response = 213; CreateContext create_context = 232; - CreateContextResponse create_context_response = 233; // current max + CreateContextResponse create_context_response = 233; UpdateContext update_context = 214; SynchronizeContexts synchronize_contexts = 215; SynchronizeContextsResponse synchronize_contexts_response = 216; @@ -1680,6 +1681,15 @@ message GetPrivateUserInfoResponse { repeated string flags = 3; } +enum Plan { + Free = 0; + ZedPro = 1; +} + +message UpdateUserPlan { + Plan plan = 1; +} + // Entities message ViewId { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 451292308d..17bf73e0bd 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -359,6 +359,7 @@ messages!( (UpdateParticipantLocation, Foreground), (UpdateProject, Foreground), (UpdateProjectCollaborator, Foreground), + (UpdateUserPlan, Foreground), (UpdateWorktree, Foreground), (UpdateWorktreeSettings, Foreground), (UsersResponse, Foreground), diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 7485024229..c837b74dca 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -36,6 +36,7 @@ command_palette.workspace = true dev_server_projects.workspace = true extensions_ui.workspace = true feedback.workspace = true +feature_flags.workspace = true gpui.workspace = true notifications.workspace = true project.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 8b88b66cdc..fd3f01e5f7 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -11,6 +11,7 @@ use crate::platforms::{platform_linux, platform_mac, platform_windows}; use auto_update::AutoUpdateStatus; use call::ActiveCall; use client::{Client, UserStore}; +use feature_flags::{FeatureFlagAppExt, ZedPro}; use gpui::{ actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement, Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful, @@ -18,7 +19,7 @@ use gpui::{ }; use project::{Project, RepositoryEntry}; use recent_projects::RecentProjects; -use rpc::proto::DevServerStatus; +use rpc::proto::{self, DevServerStatus}; use smallvec::SmallVec; use std::sync::Arc; use theme::ActiveTheme; @@ -507,16 +508,32 @@ impl TitleBar { } pub fn render_user_menu_button(&mut self, cx: &mut ViewContext) -> impl Element { - if let Some(user) = self.user_store.read(cx).current_user() { + let user_store = self.user_store.read(cx); + if let Some(user) = user_store.current_user() { + let plan = user_store.current_plan(); PopoverMenu::new("user-menu") - .menu(|cx| { - ContextMenu::build(cx, |menu, _| { - menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) + .menu(move |cx| { + ContextMenu::build(cx, |menu, cx| { + menu.when(cx.has_flag::(), |menu| { + menu.action( + format!( + "Current Plan: {}", + match plan { + None => "", + Some(proto::Plan::Free) => "Free", + Some(proto::Plan::ZedPro) => "Pro", + } + ), + zed_actions::OpenAccountSettings.boxed_clone(), + ) .separator() - .action("Sign Out", client::SignOut.boxed_clone()) + }) + .action("Settings", zed_actions::OpenSettings.boxed_clone()) + .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) + .action("Themes…", theme_selector::Toggle::default().boxed_clone()) + .action("Extensions", extensions_ui::Extensions.boxed_clone()) + .separator() + .action("Sign Out", client::SignOut.boxed_clone()) }) .into() }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 15a2013444..6d7b47a158 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -47,7 +47,7 @@ use workspace::{ open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, }; use workspace::{notifications::DetachAndPromptErr, Pane}; -use zed_actions::{OpenBrowser, OpenSettings, OpenZedUrl, Quit}; +use zed_actions::{OpenAccountSettings, OpenBrowser, OpenSettings, OpenZedUrl, Quit}; actions!( zed, @@ -422,6 +422,12 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { ); }, ) + .register_action( + |_: &mut Workspace, _: &OpenAccountSettings, cx: &mut ViewContext| { + let server_url = &client::ClientSettings::get_global(cx).server_url; + cx.open_url(&format!("{server_url}/settings")); + }, + ) .register_action( move |_: &mut Workspace, _: &OpenTasks, cx: &mut ViewContext| { open_settings_file( diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 7e2c8a096e..ec7847c848 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -26,6 +26,7 @@ actions!( zed, [ OpenSettings, + OpenAccountSettings, Quit, OpenKeymap, About,