Account · Billing and team: plans, quotas, cards, bills, prompts, members, invitations

SubscriptionCard shows a plan with its state, price and what comes
next, a way to change it, and Cancel after a second press or Keep plan
once canceled. UsageQuota gives each limit a meter and a line of how
much, or how far past. PaymentMethodForm tells a card's network from
its number, checks it with Luhn, and saves once the number checks out,
the month is ahead, the code is whole and a name is given; it rests
while the owner checks. BillingHistory lists bills with their state and
a download; UpgradePrompt stands as one line or as a paywall.
TeamMemberTable changes a member's role through RoleSelector and
removes after a second press, the owner staying; InvitationList
invites by a reading address with a role and resends or revokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 13:49:17 -04:00
1 parent b3ce50ecab
commit 6f3d8036da
10 files changed
+1863 -8

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
use std::rc::Rc;
use gpui::{
App, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window, div,
};
use super::login::Run;
use crate::{
buttons::{Button, ButtonVariant},
layout::Card,
primitives::{Icon, IconName},
theme::{ActiveTheme, IconSize, TextSize},
};
/// A way past a limit: why, what the next plan brings, and Upgrade. With benefits it stands as a paywall; without, as one line.
#[derive(IntoElement)]
pub struct UpgradePrompt {
id: ElementId,
title: SharedString,
body: Option<SharedString>,
benefits: Vec<SharedString>,
on_upgrade: Option<Run>,
on_dismiss: Option<Run>,
}
impl UpgradePrompt {
pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
title: title.into(),
body: None,
benefits: Vec::new(),
on_upgrade: None,
on_dismiss: None,
}
}
pub fn body(mut self, body: impl Into<SharedString>) -> Self {
self.body = Some(body.into());
self
}
/// Something the next plan brings; any makes this a paywall.
pub fn benefit(mut self, benefit: impl Into<SharedString>) -> Self {
self.benefits.push(benefit.into());
self
}
pub fn on_upgrade(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_upgrade = Some(Rc::new(handler));
self
}
/// Shows "Not now".
pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_dismiss = Some(Rc::new(handler));
self
}
}
impl RenderOnce for UpgradePrompt {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.id;
let upgrade = self
.on_upgrade
.unwrap_or_else(|| panic!("upgrade prompt {id:?} has no on_upgrade"));
let theme = cx.theme();
let wall = !self.benefits.is_empty();
let actions = div()
.flex()
.flex_wrap()
.gap_2()
.child(
Button::new((id.clone(), "upgrade"), "Upgrade")
.variant(ButtonVariant::Primary)
.on_click(move |_, window, cx| {
log::info!("upgrade prompt: upgrade");
upgrade(window, cx);
}),
)
.children(self.on_dismiss.map(|run| {
Button::new((id.clone(), "dismiss"), "Not now")
.variant(ButtonVariant::Ghost)
.on_click(move |_, window, cx| run(window, cx))
}));
let words = div()
.flex_1()
.min_w(theme.label_width())
.child(div().font_weight(FontWeight::SEMIBOLD).child(self.title))
.children(
self.body
.map(|body| div().text_color(theme.colors.fg_muted).child(body)),
);
let benefits = self.benefits.into_iter().map(|benefit| {
div()
.flex()
.items_center()
.gap_2()
.child(
Icon::new(IconName::Check)
.size(IconSize::Sm)
.color(theme.colors.success),
)
.child(div().flex_1().min_w_0().child(benefit))
});
let body = match wall {
true => div()
.flex()
.flex_col()
.gap_4()
.child(
div()
.flex()
.items_start()
.gap_3()
.child(
Icon::new(IconName::Lock)
.size(IconSize::Md)
.color(theme.colors.fg_muted),
)
.child(words),
)
.child(div().flex().flex_col().gap_1p5().children(benefits))
.child(actions),
false => div()
.flex()
.flex_wrap()
.items_center()
.gap_3()
.child(words)
.child(actions),
};
Card::new().child(div().text_size(theme.text_size(TextSize::Sm)).child(body))
}
}