title_bar: Show the plan from the CloudUserStore (#35401)

This PR updates the user menu in the title bar to show the plan from the
`CloudUserStore` instead of the `UserStore`.

We're still leveraging the RPC connection to listen for `UpdateUserPlan`
messages so that we can get live-updates from the server, but we are
merely using this as a signal to re-fetch the information from Cloud.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-07-31 09:56:53 -04:00 committed by GitHub
parent 89ed0b9601
commit 558bbfffae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 94 additions and 19 deletions

1
Cargo.lock generated
View file

@ -16537,6 +16537,7 @@ dependencies = [
"call",
"chrono",
"client",
"cloud_llm_client",
"collections",
"db",
"gpui",

View file

@ -2,19 +2,36 @@ use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use cloud_api_client::{AuthenticatedUser, CloudApiClient};
use gpui::{Context, Task};
use chrono::{DateTime, Utc};
use cloud_api_client::{AuthenticatedUser, CloudApiClient, GetAuthenticatedUserResponse, PlanInfo};
use cloud_llm_client::Plan;
use gpui::{Context, Entity, Subscription, Task};
use util::{ResultExt as _, maybe};
use crate::UserStore;
use crate::user::Event as RpcUserStoreEvent;
pub struct CloudUserStore {
cloud_client: Arc<CloudApiClient>,
authenticated_user: Option<Arc<AuthenticatedUser>>,
plan_info: Option<Arc<PlanInfo>>,
_maintain_authenticated_user_task: Task<()>,
_rpc_plan_updated_subscription: Subscription,
}
impl CloudUserStore {
pub fn new(cloud_client: Arc<CloudApiClient>, cx: &mut Context<Self>) -> Self {
pub fn new(
cloud_client: Arc<CloudApiClient>,
rpc_user_store: Entity<UserStore>,
cx: &mut Context<Self>,
) -> Self {
let rpc_plan_updated_subscription =
cx.subscribe(&rpc_user_store, Self::handle_rpc_user_store_event);
Self {
cloud_client: cloud_client.clone(),
authenticated_user: None,
plan_info: None,
_maintain_authenticated_user_task: cx.spawn(async move |this, cx| {
maybe!(async move {
loop {
@ -36,14 +53,15 @@ impl CloudUserStore {
.context("failed to fetch authenticated user");
if let Some(response) = authenticated_user_result.log_err() {
this.update(cx, |this, _cx| {
this.authenticated_user = Some(Arc::new(response.user));
this.update_authenticated_user(response);
})
.ok();
}
}
} else {
this.update(cx, |this, _cx| {
this.authenticated_user = None;
this.authenticated_user.take();
this.plan_info.take();
})
.ok();
}
@ -56,6 +74,7 @@ impl CloudUserStore {
.await
.log_err();
}),
_rpc_plan_updated_subscription: rpc_plan_updated_subscription,
}
}
@ -66,4 +85,56 @@ impl CloudUserStore {
pub fn authenticated_user(&self) -> Option<Arc<AuthenticatedUser>> {
self.authenticated_user.clone()
}
pub fn plan(&self) -> Option<Plan> {
self.plan_info.as_ref().map(|plan| plan.plan)
}
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
self.plan_info
.as_ref()
.and_then(|plan| plan.subscription_period)
.map(|subscription_period| {
(
subscription_period.started_at.0,
subscription_period.ended_at.0,
)
})
}
fn update_authenticated_user(&mut self, response: GetAuthenticatedUserResponse) {
self.authenticated_user = Some(Arc::new(response.user));
self.plan_info = Some(Arc::new(response.plan));
}
fn handle_rpc_user_store_event(
&mut self,
_: Entity<UserStore>,
event: &RpcUserStoreEvent,
cx: &mut Context<Self>,
) {
match event {
RpcUserStoreEvent::PlanUpdated => {
cx.spawn(async move |this, cx| {
let cloud_client =
cx.update(|cx| this.read_with(cx, |this, _cx| this.cloud_client.clone()))??;
let response = cloud_client
.get_authenticated_user()
.await
.context("failed to fetch authenticated user")?;
cx.update(|cx| {
this.update(cx, |this, _cx| {
this.update_authenticated_user(response);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
_ => {}
}
}
}

View file

@ -145,6 +145,7 @@ pub enum Event {
ShowContacts,
ParticipantIndicesChanged,
PrivateUserInfoUpdated,
PlanUpdated,
}
#[derive(Clone, Copy)]
@ -388,6 +389,7 @@ impl UserStore {
.map(EditPredictionUsage);
}
cx.emit(Event::PlanUpdated);
cx.notify();
})?;
Ok(())

View file

@ -282,7 +282,8 @@ impl TestServer {
.register_hosting_provider(Arc::new(git_hosting_providers::Github::public_instance()));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let cloud_user_store =
cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let session = cx.new(|cx| AppSession::new(Session::test(), cx));

View file

@ -32,6 +32,7 @@ auto_update.workspace = true
call.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
db.workspace = true
gpui = { workspace = true, features = ["screen-capture"] }
notifications.workspace = true

View file

@ -21,6 +21,7 @@ use crate::application_menu::{
use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, CloudUserStore, UserStore, zed_urls};
use cloud_llm_client::Plan;
use gpui::{
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
@ -28,7 +29,6 @@ use gpui::{
};
use onboarding_banner::OnboardingBanner;
use project::Project;
use rpc::proto;
use settings::Settings as _;
use settings_ui::keybindings;
use std::sync::Arc;
@ -634,8 +634,8 @@ impl TitleBar {
pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
let cloud_user_store = self.cloud_user_store.read(cx);
if let Some(user) = cloud_user_store.authenticated_user() {
let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
let plan = self.user_store.read(cx).current_plan().filter(|_| {
let has_subscription_period = cloud_user_store.subscription_period().is_some();
let plan = cloud_user_store.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
});
@ -662,13 +662,9 @@ impl TitleBar {
let user_login = user.github_login.clone();
let (plan_name, label_color, bg_color) = match plan {
None | Some(proto::Plan::Free) => {
("Free", Color::Default, free_chip_bg)
}
Some(proto::Plan::ZedProTrial) => {
("Pro Trial", Color::Accent, pro_chip_bg)
}
Some(proto::Plan::ZedPro) => ("Pro", Color::Accent, pro_chip_bg),
None | Some(Plan::ZedFree) => ("Free", Color::Default, free_chip_bg),
Some(Plan::ZedProTrial) => ("Pro Trial", Color::Accent, pro_chip_bg),
Some(Plan::ZedPro) => ("Pro", Color::Accent, pro_chip_bg),
};
menu.custom_entry(

View file

@ -913,7 +913,8 @@ impl AppState {
let client = Client::new(clock, http_client.clone(), cx);
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let cloud_user_store =
cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
@ -5738,7 +5739,8 @@ impl Workspace {
let client = project.read(cx).client();
let user_store = project.read(cx).user_store();
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let cloud_user_store =
cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let session = cx.new(|cx| AppSession::new(Session::test(), cx));

View file

@ -457,7 +457,8 @@ pub fn main() {
language::init(cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let cloud_user_store =
cx.new(|cx| CloudUserStore::new(client.cloud_client(), user_store.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
language_extension::init(