899 lines
35 KiB
Rust
899 lines
35 KiB
Rust
use std::sync::Arc;
|
|
|
|
use enclose::enclose;
|
|
use itertools::Itertools as _;
|
|
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
|
use pathfinder_color::ColorU;
|
|
use pathfinder_geometry::vector::vec2f;
|
|
use warp_core::ui::appearance::Appearance;
|
|
use warp_core::ui::Icon;
|
|
use warp_graphql::billing::AddonCreditsOption;
|
|
use warpui::elements::{
|
|
Align, Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius,
|
|
CrossAxisAlignment, DropShadow, Expanded, Flex, FormattedTextElement, HighlightedHyperlink,
|
|
Hoverable, Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
|
OffsetPositioning, ParentAnchor, ParentElement as _, ParentOffsetBounds, Radius, Shrinkable,
|
|
SizeConstraintCondition, SizeConstraintSwitch, Stack, Text,
|
|
};
|
|
use warpui::fonts::Weight;
|
|
use warpui::ui_components::button::ButtonVariant;
|
|
use warpui::ui_components::components::{Coords, UiComponent as _, UiComponentStyles};
|
|
use warpui::{AppContext, Element, Entity, SingletonEntity as _, View, ViewContext, ViewHandle};
|
|
|
|
use crate::ai::request_usage_model::{
|
|
AIRequestUsageModel, AIRequestUsageModelEvent, BuyCreditsBannerDisplayState,
|
|
};
|
|
use crate::auth::AuthStateProvider;
|
|
use crate::features::FeatureFlag;
|
|
use crate::menu::MenuItemFields;
|
|
use crate::pricing::{PricingInfoModel, PricingInfoModelEvent};
|
|
use crate::send_telemetry_from_ctx;
|
|
use crate::server::ids::ServerId;
|
|
use crate::server::telemetry::{OutOfCreditsBannerAction, TelemetryEvent};
|
|
use crate::settings_view::create_discount_badge;
|
|
use crate::view_components::Dropdown;
|
|
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
|
|
use warp_graphql::error::BudgetExceededError;
|
|
|
|
#[derive(Default)]
|
|
struct MouseStates {
|
|
buy_button: MouseStateHandle,
|
|
close_button: MouseStateHandle,
|
|
manage_billing_button: MouseStateHandle,
|
|
auto_reload_checkbox: MouseStateHandle,
|
|
auto_reload_info_icon: MouseStateHandle,
|
|
}
|
|
|
|
pub struct BuyCreditsBanner {
|
|
mouse_states: MouseStates,
|
|
denomination_dropdown: ViewHandle<Dropdown<Action>>,
|
|
addon_credits_options: Vec<AddonCreditsOption>,
|
|
selected_denomination_index: usize,
|
|
purchase_addon_credits_loading: bool,
|
|
should_display_banner: bool,
|
|
billing_settings_hyperlink: HighlightedHyperlink,
|
|
is_denomination_dropdown_open: bool,
|
|
auto_reload_enabled: bool,
|
|
banner_auto_reload_update_in_flight: bool,
|
|
}
|
|
|
|
impl BuyCreditsBanner {
|
|
pub fn is_denomination_dropdown_open(&self, _app: &AppContext) -> bool {
|
|
self.is_denomination_dropdown_open
|
|
}
|
|
|
|
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
|
ctx.subscribe_to_model(&PricingInfoModel::handle(ctx), |me, _handle, event, ctx| {
|
|
#[allow(irrefutable_let_patterns)]
|
|
if let PricingInfoModelEvent::PricingInfoUpdated = event {
|
|
me.update_addon_credits_options(ctx);
|
|
ctx.notify();
|
|
}
|
|
});
|
|
|
|
ctx.subscribe_to_model(
|
|
&AIRequestUsageModel::handle(ctx),
|
|
|_me, _handle, event, ctx| {
|
|
if let AIRequestUsageModelEvent::RequestUsageUpdated = event {
|
|
ctx.notify();
|
|
}
|
|
},
|
|
);
|
|
|
|
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _handle, event, ctx| {
|
|
me.handle_workspaces_event(event, ctx);
|
|
});
|
|
|
|
let denomination_dropdown = ctx.add_typed_action_view(|ctx| {
|
|
let mut dropdown = Dropdown::new(ctx).with_drop_shadow();
|
|
dropdown.set_top_bar_max_width(170.);
|
|
dropdown.set_menu_width(225., ctx);
|
|
dropdown
|
|
});
|
|
|
|
ctx.subscribe_to_view(&denomination_dropdown, |me, _, event, ctx| match event {
|
|
crate::view_components::DropdownEvent::ToggleExpanded => {
|
|
me.is_denomination_dropdown_open = true;
|
|
ctx.notify();
|
|
}
|
|
crate::view_components::DropdownEvent::Close => {
|
|
me.is_denomination_dropdown_open = false;
|
|
ctx.emit(BuyCreditsBannerEvent::RefocusInput);
|
|
ctx.notify();
|
|
}
|
|
});
|
|
|
|
let mut me = Self {
|
|
mouse_states: Default::default(),
|
|
denomination_dropdown,
|
|
addon_credits_options: Default::default(),
|
|
selected_denomination_index: 0,
|
|
purchase_addon_credits_loading: false,
|
|
should_display_banner: false,
|
|
billing_settings_hyperlink: HighlightedHyperlink::default(),
|
|
is_denomination_dropdown_open: false,
|
|
auto_reload_enabled: false,
|
|
banner_auto_reload_update_in_flight: false,
|
|
};
|
|
me.update_addon_credits_options(ctx);
|
|
me
|
|
}
|
|
|
|
fn handle_workspaces_event(
|
|
&mut self,
|
|
event: &UserWorkspacesEvent,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
match event {
|
|
UserWorkspacesEvent::PurchaseAddonCreditsSuccess => {
|
|
self.purchase_addon_credits_loading = false;
|
|
|
|
let banner_toggle_flag_enabled =
|
|
FeatureFlag::BuildPlanAutoReloadBannerToggle.is_enabled();
|
|
let post_purchase_modal_flag_enabled =
|
|
FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.is_enabled();
|
|
|
|
let selected_credits = self
|
|
.addon_credits_options
|
|
.get(self.selected_denomination_index)
|
|
.map(|option| option.credits);
|
|
|
|
// Things we always do:
|
|
// - emit telemetry
|
|
// - hide banner
|
|
send_telemetry_from_ctx!(
|
|
TelemetryEvent::OutOfCreditsBannerClosed {
|
|
action: OutOfCreditsBannerAction::CreditsPurchased,
|
|
selected_credits,
|
|
auto_reload_checkbox_enabled: self.auto_reload_enabled,
|
|
banner_toggle_flag_enabled,
|
|
post_purchase_modal_flag_enabled,
|
|
},
|
|
ctx
|
|
);
|
|
|
|
self.should_display_banner = false;
|
|
AIRequestUsageModel::handle(ctx).update(ctx, |ai_request_usage_model, ctx| {
|
|
ai_request_usage_model.dismiss_buy_credits_banner(ctx);
|
|
});
|
|
|
|
// Experiment-specific behavior:
|
|
// - Banner toggle flow: optionally enable auto-reload immediately.
|
|
// - Post-purchase modal flow: show the modal.
|
|
if banner_toggle_flag_enabled {
|
|
if self.auto_reload_enabled {
|
|
self.banner_auto_reload_update_in_flight = true;
|
|
|
|
if let Some(team_uid) = UserWorkspaces::as_ref(ctx).current_team_uid() {
|
|
UserWorkspaces::handle(ctx).update(ctx, |user_workspaces, ctx| {
|
|
user_workspaces.update_addon_credits_settings(
|
|
team_uid,
|
|
Some(true),
|
|
None, // Don't change monthly spend limit
|
|
selected_credits,
|
|
ctx,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
} else if post_purchase_modal_flag_enabled {
|
|
// Default selection in the modal should match the denomination the user clicked "buy" on.
|
|
ctx.emit(BuyCreditsBannerEvent::OpenAutoReloadModal {
|
|
purchased_credits: selected_credits.unwrap_or(0),
|
|
});
|
|
}
|
|
|
|
ctx.notify();
|
|
}
|
|
UserWorkspacesEvent::PurchaseAddonCreditsRejected(err) => {
|
|
self.purchase_addon_credits_loading = false;
|
|
self.banner_auto_reload_update_in_flight = false;
|
|
|
|
if err.downcast_ref::<BudgetExceededError>().is_some() {
|
|
self.should_display_banner = true;
|
|
} else {
|
|
AIRequestUsageModel::handle(ctx).update(ctx, |ai_request_usage_model, ctx| {
|
|
ai_request_usage_model.dismiss_buy_credits_banner(ctx);
|
|
});
|
|
}
|
|
ctx.notify();
|
|
}
|
|
UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess => {
|
|
if self.banner_auto_reload_update_in_flight {
|
|
self.banner_auto_reload_update_in_flight = false;
|
|
ctx.notify();
|
|
}
|
|
}
|
|
UserWorkspacesEvent::UpdateWorkspaceSettingsRejected(_) => {
|
|
if self.banner_auto_reload_update_in_flight {
|
|
self.banner_auto_reload_update_in_flight = false;
|
|
ctx.emit(BuyCreditsBannerEvent::ShowAutoReloadError {
|
|
error_message: "Failed to enable auto-reload for your team. Please try again in Settings > Billing and Usage.",
|
|
});
|
|
ctx.notify();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn render_auto_reload_checkbox(&self, appearance: &Appearance) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
let check_color = theme.background().into_solid();
|
|
let auto_reload_enabled = self.auto_reload_enabled;
|
|
|
|
let checkbox = appearance
|
|
.ui_builder()
|
|
.checkbox(
|
|
self.mouse_states.auto_reload_checkbox.clone(),
|
|
Some(appearance.ui_font_size()),
|
|
)
|
|
.check(auto_reload_enabled)
|
|
.with_style(UiComponentStyles {
|
|
font_color: Some(check_color),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(Action::ToggleAutoReload);
|
|
})
|
|
.finish();
|
|
|
|
let sub_text_color = theme.sub_text_color(theme.surface_1());
|
|
|
|
let label = Text::new_inline("Auto reload", appearance.ui_font_family(), 12.)
|
|
.with_color(sub_text_color.into())
|
|
.finish();
|
|
|
|
// Get the selected amount for the tooltip
|
|
let selected_credits = self
|
|
.addon_credits_options
|
|
.get(self.selected_denomination_index)
|
|
.map(|option| option.credits)
|
|
.unwrap_or(0);
|
|
|
|
let tooltip_text = format!(
|
|
"When enabled, auto reload will purchase {} credits when your credit balance gets low",
|
|
selected_credits
|
|
);
|
|
|
|
// Create info icon with a custom sub_text_color & mouse cursor (i.e. as opposed to using IconWithTooltip)
|
|
let ui_builder = appearance.ui_builder();
|
|
let info_icon = Hoverable::new(
|
|
self.mouse_states.auto_reload_info_icon.clone(),
|
|
move |state| {
|
|
let info_icon_element = Container::new(
|
|
ConstrainedBox::new(
|
|
WarpUiIcon::new("bundled/svg/info.svg", sub_text_color).finish(),
|
|
)
|
|
.with_width(13.)
|
|
.with_height(13.)
|
|
.finish(),
|
|
)
|
|
.finish();
|
|
|
|
let mut stack = Stack::new().with_child(info_icon_element);
|
|
if state.is_hovered() {
|
|
let tool_tip = ui_builder.tool_tip(tooltip_text.clone()).build();
|
|
stack.add_positioned_child(
|
|
tool_tip.finish(),
|
|
OffsetPositioning::offset_from_parent(
|
|
vec2f(0., -3.),
|
|
ParentOffsetBounds::Unbounded,
|
|
ParentAnchor::TopMiddle,
|
|
ChildAnchor::BottomMiddle,
|
|
),
|
|
);
|
|
}
|
|
stack.finish()
|
|
},
|
|
)
|
|
.finish();
|
|
|
|
Flex::row()
|
|
.with_child(Container::new(checkbox).with_margin_right(4.).finish())
|
|
.with_child(label)
|
|
.with_child(
|
|
Container::new(info_icon)
|
|
.with_margin_left(4.)
|
|
.with_margin_bottom(1.)
|
|
.finish(),
|
|
)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.finish()
|
|
}
|
|
|
|
fn update_addon_credits_options(&mut self, ctx: &mut ViewContext<Self>) {
|
|
self.addon_credits_options = PricingInfoModel::as_ref(ctx)
|
|
.addon_credits_options()
|
|
.map(|opts| opts.to_vec())
|
|
.unwrap_or_default();
|
|
|
|
let base_rate = self
|
|
.addon_credits_options
|
|
.first()
|
|
.map_or(0., |option| option.rate());
|
|
let items = self
|
|
.addon_credits_options
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, option)| {
|
|
let primary_text = format!(
|
|
"${:.0} / {} credits",
|
|
option.price_usd_cents as f32 / 100.,
|
|
option.credits
|
|
);
|
|
let discount_percent = if base_rate > 0.0 {
|
|
let actual_rate = option.rate();
|
|
((base_rate - actual_rate) / base_rate * 100.0).round() as u32
|
|
} else {
|
|
0
|
|
};
|
|
if discount_percent > 0 {
|
|
MenuItemFields::new_with_custom_label(
|
|
Arc::new(enclose!((primary_text) move |is_selected, is_hovered, appearance, _| {
|
|
let text_color = appearance.theme().main_text_color(
|
|
if is_selected || is_hovered {
|
|
appearance.theme().accent()
|
|
} else {
|
|
appearance.theme().surface_1()
|
|
}
|
|
);
|
|
let main_text = Text::new_inline(
|
|
primary_text.clone(),
|
|
appearance.ui_font_family(),
|
|
appearance.ui_font_size(),
|
|
)
|
|
.with_color(text_color.into())
|
|
.finish();
|
|
|
|
let discount_badge = create_discount_badge(discount_percent, appearance);
|
|
|
|
Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_child(main_text)
|
|
.with_child(discount_badge)
|
|
.finish()
|
|
})),
|
|
Some(primary_text)
|
|
)
|
|
.with_on_select_action(Action::SelectDenomination(index).into())
|
|
.into_item()
|
|
} else {
|
|
MenuItemFields::new(primary_text.clone())
|
|
.with_on_select_action(Action::SelectDenomination(index).into())
|
|
.into_item()
|
|
}
|
|
})
|
|
.collect_vec();
|
|
self.denomination_dropdown.update(ctx, |dropdown, ctx| {
|
|
dropdown.set_rich_items(items, ctx);
|
|
dropdown.set_selected_by_index(0, ctx);
|
|
});
|
|
}
|
|
|
|
fn render_auto_reload_blocked(
|
|
&self,
|
|
appearance: &Appearance,
|
|
app: &AppContext,
|
|
) -> Box<dyn Element> {
|
|
let theme = appearance.theme();
|
|
|
|
let alert_icon = Container::new(
|
|
ConstrainedBox::new(
|
|
Icon::AlertCircle
|
|
.to_warpui_icon(theme.foreground())
|
|
.finish(),
|
|
)
|
|
.with_height(16.)
|
|
.with_width(16.)
|
|
.finish(),
|
|
)
|
|
.with_margin_right(8.)
|
|
.finish();
|
|
|
|
let auth_state = AuthStateProvider::as_ref(app).get();
|
|
let current_team = UserWorkspaces::as_ref(app).current_team();
|
|
let has_admin_permissions = auth_state
|
|
.user_email()
|
|
.zip(current_team)
|
|
.map(|(email, team)| team.has_admin_permissions(&email))
|
|
.unwrap_or_default();
|
|
|
|
// Banner text with title and description based on admin status
|
|
let banner_description = if has_admin_permissions {
|
|
"Your monthly spend limit has been reached. Increase it to continue."
|
|
} else {
|
|
"Contact a team admin to increase monthly limit."
|
|
};
|
|
|
|
let banner_text = Flex::column()
|
|
.with_children([
|
|
appearance
|
|
.ui_builder()
|
|
.paragraph("Monthly limit reached")
|
|
.with_style(UiComponentStyles {
|
|
font_size: Some(14.),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish(),
|
|
appearance
|
|
.ui_builder()
|
|
.paragraph(banner_description)
|
|
.with_style(UiComponentStyles {
|
|
font_color: Some(theme.sub_text_color(theme.surface_1()).into()),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish(),
|
|
])
|
|
.finish();
|
|
|
|
let close_button = appearance
|
|
.ui_builder()
|
|
.close_button(16., self.mouse_states.close_button.clone())
|
|
.build()
|
|
.on_click(|ctx, _, _| ctx.dispatch_typed_action(Action::Close))
|
|
.finish();
|
|
|
|
let mut content = Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_children([
|
|
alert_icon,
|
|
Shrinkable::new(1., Align::new(banner_text).left().finish()).finish(),
|
|
]);
|
|
|
|
// Only show manage billing button for admins
|
|
if has_admin_permissions {
|
|
let manage_billing_button = appearance
|
|
.ui_builder()
|
|
.button(
|
|
ButtonVariant::Accent,
|
|
self.mouse_states.manage_billing_button.clone(),
|
|
)
|
|
.with_style(UiComponentStyles {
|
|
font_weight: Some(Weight::Semibold),
|
|
padding: Some(Coords {
|
|
top: 6.,
|
|
bottom: 6.,
|
|
left: 8.,
|
|
right: 8.,
|
|
}),
|
|
..Default::default()
|
|
})
|
|
.with_text_label("Manage billing".to_string())
|
|
.build()
|
|
.on_click(|ctx, _, _| {
|
|
ctx.dispatch_typed_action(Action::ManageBilling);
|
|
})
|
|
.finish();
|
|
|
|
content.add_child(
|
|
Container::new(manage_billing_button)
|
|
.with_margin_right(8.)
|
|
.finish(),
|
|
);
|
|
}
|
|
|
|
content.add_child(close_button);
|
|
|
|
Container::new(content.finish())
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
|
.with_border(Border::all(1.).with_border_fill(theme.outline()))
|
|
.with_background_color(theme.surface_1().into())
|
|
.with_horizontal_padding(16.)
|
|
.with_vertical_padding(12.)
|
|
.with_horizontal_margin(8.)
|
|
.with_drop_shadow(DropShadow::new_with_standard_offset_and_spread(
|
|
ColorU::new(0, 0, 0, 48),
|
|
))
|
|
.finish()
|
|
}
|
|
|
|
fn render_out_of_credits(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
|
// When the auto-reload checkbox is present, we need more horizontal space before we can
|
|
// fit everything on one row.
|
|
const STACKED_LAYOUT_MAX_WIDTH_WITH_AUTO_RELOAD: f32 = 520.;
|
|
const STACKED_LAYOUT_MAX_WIDTH_WITHOUT_AUTO_RELOAD: f32 = 400.;
|
|
|
|
let theme = appearance.theme();
|
|
|
|
let make_alert_icon = |margin_bottom: f32| {
|
|
Container::new(
|
|
ConstrainedBox::new(
|
|
Icon::AlertCircle
|
|
.to_warpui_icon(theme.foreground())
|
|
.finish(),
|
|
)
|
|
.with_height(16.)
|
|
.with_width(16.)
|
|
.finish(),
|
|
)
|
|
.with_margin_right(8.)
|
|
.with_margin_bottom(margin_bottom)
|
|
.finish()
|
|
};
|
|
|
|
let auth_state = AuthStateProvider::as_ref(app).get();
|
|
let current_team = UserWorkspaces::as_ref(app).current_team();
|
|
let has_admin_permissions = auth_state
|
|
.user_email()
|
|
.zip(current_team)
|
|
.map(|(email, team)| team.has_admin_permissions(&email))
|
|
.unwrap_or_default();
|
|
let delinquent_due_to_payment_issue = current_team
|
|
.is_some_and(|team| team.billing_metadata.is_delinquent_due_to_payment_issue());
|
|
let auto_reload_banner_toggle_ff =
|
|
FeatureFlag::BuildPlanAutoReloadBannerToggle.is_enabled();
|
|
|
|
// Check if user has reached their monthly addon credits limit
|
|
let current_workspace = UserWorkspaces::as_ref(app).current_workspace();
|
|
let is_at_monthly_limit = current_workspace
|
|
.map(|workspace| workspace.is_at_addon_credits_monthly_limit())
|
|
.unwrap_or(false);
|
|
|
|
// Check if the selected purchase would reach/exceed the monthly limit
|
|
let selected_option = self
|
|
.addon_credits_options
|
|
.get(self.selected_denomination_index);
|
|
let would_purchase_exceed_limit = current_workspace
|
|
.zip(selected_option)
|
|
.map(|(workspace, option)| {
|
|
workspace.would_addon_purchase_reach_limit(option.price_usd_cents)
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
let make_banner_text = || {
|
|
let mut banner_text_children = vec![appearance
|
|
.ui_builder()
|
|
.paragraph("Out of credits")
|
|
.with_style(UiComponentStyles {
|
|
font_size: Some(14.),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish()];
|
|
|
|
// Show different message based on whether purchase would exceed limit
|
|
if is_at_monthly_limit || would_purchase_exceed_limit {
|
|
// Create formatted text with clickable hyperlink
|
|
let warning_text_fragments = vec![
|
|
FormattedTextFragment::plain_text(
|
|
"Purchasing these credits would take you over your monthly spend limit. ",
|
|
),
|
|
FormattedTextFragment::hyperlink_action("Increase it", Action::ManageBilling),
|
|
FormattedTextFragment::plain_text(" to continue."),
|
|
];
|
|
|
|
let formatted_warning = FormattedTextElement::new(
|
|
FormattedText::new([FormattedTextLine::Line(warning_text_fragments)]),
|
|
appearance.ui_font_size(),
|
|
appearance.ui_font_family(),
|
|
appearance.ui_font_family(),
|
|
theme.sub_text_color(theme.surface_1()).into(),
|
|
self.billing_settings_hyperlink.clone(),
|
|
)
|
|
.with_hyperlink_font_color(theme.accent().into_solid())
|
|
.register_default_click_handlers_with_action_support(
|
|
|hyperlink_lens, event, _ctx| match hyperlink_lens {
|
|
warpui::elements::HyperlinkLens::Url(_url) => {}
|
|
warpui::elements::HyperlinkLens::Action(action_ref) => {
|
|
if let Some(action) = action_ref.as_any().downcast_ref::<Action>() {
|
|
event.dispatch_typed_action(action.clone());
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.finish();
|
|
|
|
banner_text_children.push(formatted_warning);
|
|
} else {
|
|
// Default message when not at limit
|
|
let banner_description = if has_admin_permissions {
|
|
"Add more credits to your account to continue using Oz agents."
|
|
} else {
|
|
"Contact a team admin to purchase more credits to continue."
|
|
};
|
|
|
|
banner_text_children.push(
|
|
appearance
|
|
.ui_builder()
|
|
.paragraph(banner_description)
|
|
.with_style(UiComponentStyles {
|
|
font_color: Some(theme.sub_text_color(theme.surface_1()).into()),
|
|
..Default::default()
|
|
})
|
|
.build()
|
|
.finish(),
|
|
);
|
|
}
|
|
|
|
let banner_text =
|
|
Container::new(Flex::column().with_children(banner_text_children).finish())
|
|
.with_margin_right(8.)
|
|
.finish();
|
|
|
|
Expanded::new(
|
|
1.,
|
|
Align::new(ConstrainedBox::new(banner_text).finish())
|
|
.left()
|
|
.finish(),
|
|
)
|
|
.finish()
|
|
};
|
|
|
|
let make_buy_button = || {
|
|
let team_uid = current_team.map(|team| team.uid);
|
|
|
|
let buy_button_disabled = self.purchase_addon_credits_loading
|
|
|| delinquent_due_to_payment_issue
|
|
|| is_at_monthly_limit
|
|
|| would_purchase_exceed_limit;
|
|
|
|
let button_text = if self.purchase_addon_credits_loading {
|
|
"Buying…".to_string()
|
|
} else {
|
|
"Buy".to_string()
|
|
};
|
|
|
|
let button_font_color = buy_button_disabled.then_some(
|
|
appearance
|
|
.theme()
|
|
.disabled_text_color(appearance.theme().surface_3())
|
|
.into(),
|
|
);
|
|
let button_bg_color =
|
|
buy_button_disabled.then_some(appearance.theme().surface_3().into());
|
|
let button_border = buy_button_disabled.then_some(ColorU::transparent_black().into());
|
|
|
|
let mut buy_button = appearance
|
|
.ui_builder()
|
|
.button(ButtonVariant::Accent, self.mouse_states.buy_button.clone())
|
|
.with_style(UiComponentStyles {
|
|
font_weight: Some(Weight::Semibold),
|
|
padding: Some(Coords {
|
|
top: 6.,
|
|
bottom: 6.,
|
|
left: 8.,
|
|
right: 8.,
|
|
}),
|
|
font_color: button_font_color,
|
|
background: button_bg_color,
|
|
border_color: button_border,
|
|
..Default::default()
|
|
})
|
|
.with_text_label(button_text)
|
|
.build()
|
|
.on_click(move |ctx, _, _| {
|
|
if let Some(team_uid) = team_uid {
|
|
ctx.dispatch_typed_action(Action::PurchaseAddonCredits { team_uid });
|
|
}
|
|
});
|
|
|
|
if buy_button_disabled {
|
|
buy_button = buy_button.disable();
|
|
}
|
|
|
|
buy_button.finish()
|
|
};
|
|
|
|
let make_close_button = || {
|
|
Container::new(
|
|
appearance
|
|
.ui_builder()
|
|
.close_button(16., self.mouse_states.close_button.clone())
|
|
.build()
|
|
.on_click(|ctx, _, _| ctx.dispatch_typed_action(Action::Close))
|
|
.finish(),
|
|
)
|
|
.with_margin_left(8.)
|
|
.finish()
|
|
};
|
|
|
|
let make_admin_controls_children = || {
|
|
let denomination_dropdown = ChildView::new(&self.denomination_dropdown).finish();
|
|
let buy_button = make_buy_button();
|
|
|
|
let mut children = Vec::new();
|
|
|
|
if auto_reload_banner_toggle_ff {
|
|
children.push(
|
|
Container::new(self.render_auto_reload_checkbox(appearance))
|
|
.with_margin_right(8.)
|
|
.finish(),
|
|
);
|
|
}
|
|
|
|
children.extend([
|
|
Container::new(denomination_dropdown)
|
|
.with_margin_right(4.)
|
|
.finish(),
|
|
Container::new(buy_button).finish(),
|
|
]);
|
|
|
|
children
|
|
};
|
|
|
|
let make_admin_controls_row = || {
|
|
Flex::row()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_main_axis_alignment(MainAxisAlignment::End)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_children(make_admin_controls_children())
|
|
.finish()
|
|
};
|
|
|
|
let make_row_layout = || {
|
|
let mut content = Flex::row()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_children([make_alert_icon(12.), make_banner_text()]);
|
|
|
|
if has_admin_permissions {
|
|
content.add_children(make_admin_controls_children());
|
|
}
|
|
|
|
content.add_child(make_close_button());
|
|
content.finish()
|
|
};
|
|
|
|
let make_stacked_layout = || {
|
|
let top_row = Flex::row()
|
|
.with_main_axis_size(MainAxisSize::Max)
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_children([make_alert_icon(0.), make_banner_text(), make_close_button()])
|
|
.finish();
|
|
|
|
let bottom_row = Container::new(make_admin_controls_row())
|
|
.with_margin_top(8.)
|
|
.finish();
|
|
|
|
Flex::column().with_children([top_row, bottom_row]).finish()
|
|
};
|
|
|
|
let content = if has_admin_permissions {
|
|
let stacked_breakpoint = if auto_reload_banner_toggle_ff {
|
|
STACKED_LAYOUT_MAX_WIDTH_WITH_AUTO_RELOAD
|
|
} else {
|
|
STACKED_LAYOUT_MAX_WIDTH_WITHOUT_AUTO_RELOAD
|
|
};
|
|
|
|
SizeConstraintSwitch::new(
|
|
make_row_layout(),
|
|
vec![(
|
|
SizeConstraintCondition::WidthLessThan(stacked_breakpoint),
|
|
make_stacked_layout(),
|
|
)],
|
|
)
|
|
.finish()
|
|
} else {
|
|
make_row_layout()
|
|
};
|
|
|
|
// The dropdown adds some margin so we need to use less padding.
|
|
let vertical_padding = if has_admin_permissions { 6. } else { 12. };
|
|
|
|
Container::new(content)
|
|
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
|
.with_border(Border::all(1.).with_border_fill(theme.outline()))
|
|
.with_background_color(theme.surface_1().into())
|
|
.with_horizontal_padding(16.)
|
|
.with_vertical_padding(vertical_padding)
|
|
.with_horizontal_margin(8.)
|
|
.with_drop_shadow(DropShadow::default())
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum BuyCreditsBannerEvent {
|
|
OpenBillingAndUsage,
|
|
RefocusInput,
|
|
OpenAutoReloadModal { purchased_credits: i32 },
|
|
ShowAutoReloadError { error_message: &'static str },
|
|
}
|
|
|
|
impl Entity for BuyCreditsBanner {
|
|
type Event = BuyCreditsBannerEvent;
|
|
}
|
|
|
|
impl View for BuyCreditsBanner {
|
|
fn ui_name() -> &'static str {
|
|
"BuyCreditsBanner"
|
|
}
|
|
|
|
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
|
let appearance = Appearance::as_ref(app);
|
|
let ai_request_usage = AIRequestUsageModel::as_ref(app);
|
|
|
|
// Override with spend limit error if set (from failed purchase attempt)
|
|
let display_state = if self.should_display_banner {
|
|
BuyCreditsBannerDisplayState::MonthlyLimitReached
|
|
} else {
|
|
ai_request_usage.compute_buy_addon_credits_banner_display_state(app)
|
|
};
|
|
|
|
match display_state {
|
|
BuyCreditsBannerDisplayState::Hidden => {
|
|
Container::new(warpui::elements::Empty::new().finish()).finish()
|
|
}
|
|
BuyCreditsBannerDisplayState::OutOfCredits => {
|
|
self.render_out_of_credits(appearance, app)
|
|
}
|
|
BuyCreditsBannerDisplayState::MonthlyLimitReached => {
|
|
self.render_auto_reload_blocked(appearance, app)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Action {
|
|
SelectDenomination(usize),
|
|
Close,
|
|
PurchaseAddonCredits { team_uid: ServerId },
|
|
ManageBilling,
|
|
ToggleAutoReload,
|
|
}
|
|
|
|
impl warpui::TypedActionView for BuyCreditsBanner {
|
|
type Action = Action;
|
|
|
|
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
|
match action {
|
|
Action::SelectDenomination(index) => {
|
|
self.selected_denomination_index = *index;
|
|
ctx.notify();
|
|
}
|
|
Action::Close => {
|
|
send_telemetry_from_ctx!(
|
|
TelemetryEvent::OutOfCreditsBannerClosed {
|
|
action: OutOfCreditsBannerAction::Dismissed,
|
|
selected_credits: None,
|
|
auto_reload_checkbox_enabled: self.auto_reload_enabled,
|
|
banner_toggle_flag_enabled: FeatureFlag::BuildPlanAutoReloadBannerToggle
|
|
.is_enabled(),
|
|
post_purchase_modal_flag_enabled:
|
|
FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.is_enabled(),
|
|
},
|
|
ctx
|
|
);
|
|
|
|
self.should_display_banner = false;
|
|
AIRequestUsageModel::handle(ctx).update(ctx, |model, ctx| {
|
|
model.dismiss_buy_credits_banner(ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
Action::ManageBilling => {
|
|
ctx.emit(BuyCreditsBannerEvent::OpenBillingAndUsage);
|
|
self.should_display_banner = false;
|
|
AIRequestUsageModel::handle(ctx).update(ctx, |model, ctx| {
|
|
model.dismiss_buy_credits_banner(ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
Action::PurchaseAddonCredits { team_uid } => {
|
|
if let Some(option) = self
|
|
.addon_credits_options
|
|
.get(self.selected_denomination_index)
|
|
{
|
|
let credits = option.credits;
|
|
self.purchase_addon_credits_loading = true;
|
|
UserWorkspaces::handle(ctx).update(ctx, |user_workspaces, ctx| {
|
|
user_workspaces.purchase_addon_credits(*team_uid, credits, ctx);
|
|
});
|
|
ctx.notify();
|
|
}
|
|
}
|
|
Action::ToggleAutoReload => {
|
|
self.auto_reload_enabled = !self.auto_reload_enabled;
|
|
ctx.notify();
|
|
}
|
|
}
|
|
}
|
|
}
|