Merge pull request #37241 from sokumon/billing-info

refactor: billing components
This commit is contained in:
Soham Kulkarni 2026-02-20 02:50:04 +05:30 committed by GitHub
commit 7639a87160
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 156 additions and 165 deletions

View file

@ -19,7 +19,7 @@ from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
from frappe.email.inbox import get_email_accounts
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
from frappe.integrations.frappe_providers.frappecloud_billing import current_site_info, is_fc_site
from frappe.model.base_document import get_controller
from frappe.permissions import has_permission
from frappe.query_builder import DocType
@ -125,6 +125,8 @@ def get_bootinfo():
bootinfo.setup_wizard_completed_apps = get_setup_wizard_completed_apps() or []
bootinfo.desktop_icon_urls = get_desktop_icon_urls()
bootinfo.desktop_icon_style = get_icon_style() or "Subtle"
if bootinfo.is_fc_site:
bootinfo.site_info = current_site_info()
return bootinfo

View file

@ -80,6 +80,12 @@
margin-top: 60px;
padding: 20px;
}
.icons-container:has(.sidebar-card){
margin-top: 20px;
.sidebar-card{
gap: 6px;
}
}
.modal
.modal-body .icons-container,.folder-icon .icons-container {
padding:0px;

View file

@ -1,6 +1,6 @@
<!-- jinja -->
<div class="desktop-wrapper">
<header class="navbar navbar-expand navbar-container" role="navigation">
<header class="desktop-navbar navbar navbar-expand navbar-container" role="navigation">
<div class="navbar-home">
<img
id="brand-logo"

View file

@ -176,8 +176,7 @@ class DesktopPage {
this.desktop_menu_items = [];
}
update() {
this.make(this.page);
this.setup();
this.make();
}
prepare() {
this.apps_icons = [];
@ -278,8 +277,8 @@ class DesktopPage {
if (this.edit_mode) {
this.start_editing_layout();
}
this.setup();
}
setup() {
$(document).trigger("desktop_screen", { desktop: this });
this.setup_avatar();

View file

@ -24,13 +24,13 @@ page_js = {"setup-wizard": "public/js/frappe/setup_wizard.js"}
# website
app_include_js = [
"libs.bundle.js",
"billing.bundle.js",
"desk.bundle.js",
"list.bundle.js",
"form.bundle.js",
"controls.bundle.js",
"report.bundle.js",
"telemetry.bundle.js",
"billing.bundle.js",
]
app_include_css = [

View file

@ -2,157 +2,83 @@ let frappeCloudBaseEndpoint = "https://frappecloud.com";
let isFCUser = false;
$(document).ready(function () {
if (
frappe.boot.is_fc_site &&
!!frappe.boot.setup_complete &&
!frappe.is_mobile() &&
frappe.user.has_role("System Manager")
) {
frappe.call({
method: "frappe.integrations.frappe_providers.frappecloud_billing.current_site_info",
callback: (r) => {
if (!r?.message) return;
const response = frappe.boot.site_info;
const trial_end_date = new Date(response.trial_end_date);
frappeCloudBaseEndpoint = response.base_url;
isFCUser = response.is_fc_user;
const response = r.message;
const trial_end_date = new Date(response.trial_end_date);
frappeCloudBaseEndpoint = response.base_url;
isFCUser = response.is_fc_user;
if (response.trial_end_date && trial_end_date > new Date()) {
addManageTrialBannerDesktop(response.trial_end_date);
}
addManageBillingDropdown();
$(".login-to-fc, .upgrade-plan-button").on("click", function () {
openFrappeCloudDashboard();
});
},
});
}
});
function setErrorMessage(message) {
$("#fc-login-error").text(message);
}
function addManageTrialBannerDesktop(trial_end_date) {
$(document).on("desktop_screen", function (event, data) {
const icons_container = data.desktop.wrapper.find(".icons-container").first();
$(".desktop-container").before(
generateTrialSubscriptionBanner(trial_end_date).css({
width: icons_container.width(),
margin: "auto",
padding: "20px 20px 0px",
})
);
icons_container.css("margin-top", "40px");
});
}
function addManageBillingDropdown() {
$(document).on("desktop_screen", function (event, data) {
data.desktop.add_menu_item({
label: __("Manage Billing"),
icon: "receipt-text",
condition: function () {
return frappe.boot.is_fc_site;
},
onClick: function () {
return openFrappeCloudDashboard();
},
});
});
}
function openFrappeCloudDashboard() {
window.open(`${frappeCloudBaseEndpoint}/dashboard/sites/${frappe.boot.sitename}`, "_blank");
}
function generateTrialSubscriptionBanner(trialEndDate) {
const trial_end_date = new Date(trialEndDate);
const today = new Date();
const diffTime = trial_end_date - today;
const trial_end_days = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
const trial_end_string =
trial_end_days > 1 ? `${trial_end_days} days` : `${trial_end_days} day`;
return $(`<div>
<style>
.trial-banner {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--subtle-accent);
border-radius: var(--border-radius-md);
}
.trial-banner > div {
display: flex;
gap: 8px;
}
.trial-banner .info-icon {
margin: auto 0;
}
.trial-banner > div > div {
display: flex;
flex-direction: column;
justify-content: center;
}
.trial-banner .title {
font-size: var(--text-base);
font-weight: var(--weight-semibold);
}
.trial-banner .description {
font-size: var(--text-sm);
color: var(--text-muted);
}
.trial-banner .upgrade-plan-button {
height: fit-content;
background-color: var(--fg-color);
border: 1px solid var(--gray-300);
color: var(--gray-800);
border-radius: var(--border-radius);
}
.trial-banner .upgrade-plan-button:hover {
border-color: var(--gray-400);
}
</style>
<div class="trial-banner px-3 py-2 m-2 mt-4">
<div>
<svg class="info-icon" width="18" height="18" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3360_13841)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 14.25C11.4518 14.25 14.25 11.4518 14.25 8C14.25 4.54822 11.4518 1.75 8 1.75C4.54822 1.75 1.75 4.54822 1.75 8C1.75 11.4518 4.54822 14.25 8 14.25ZM8 15.25C12.0041 15.25 15.25 12.0041 15.25 8C15.25 3.99594 12.0041 0.75 8 0.75C3.99594 0.75 0.75 3.99594 0.75 8C0.75 12.0041 3.99594 15.25 8 15.25ZM8 5.75C8.48325 5.75 8.875 5.35825 8.875 4.875C8.875 4.39175 8.48325 4 8 4C7.51675 4 7.125 4.39175 7.125 4.875C7.125 5.35825 7.51675 5.75 8 5.75ZM8.5 7.43555C8.5 7.1594 8.27614 6.93555 8 6.93555C7.72386 6.93555 7.5 7.1594 7.5 7.43555V11.143C7.5 11.4191 7.72386 11.643 8 11.643C8.27614 11.643 8.5 11.4191 8.5 11.143V7.43555Z" fill="#171717"/>
</g>
<defs>
<clipPath id="clip0_3360_13841">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<div>
<span class="title">
Your trial ends in ${trial_end_string}.
</span>
<span class="description">
${
isFCUser
? "Please upgrade for uninterrupted services"
: "Please contact your system administrator to upgrade your plan."
}
</span>
</div>
</div>
${
isFCUser
? `<button type="button"
class="upgrade-plan-button px-2 py-1"
>
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.2641 1C5.5758 1 4.97583 1.46845 4.80889 2.1362L3.57555 7.06953C3.33887 8.01625 4.05491 8.93333 5.03077 8.93333H7.50682L6.72168 14.4293C6.68838 14.6624 6.82229 14.8872 7.04319 14.9689C7.26408 15.0507 7.51204 14.9671 7.63849 14.7684L13.2161 6.00354C13.6398 5.33782 13.1616 4.46667 12.3725 4.46667H9.59038L10.3017 1.62127C10.3391 1.4719 10.3055 1.31365 10.2108 1.19229C10.116 1.07094 9.97063 1 9.81666 1H6.2641ZM5.77903 2.37873C5.83468 2.15615 6.03467 2 6.2641 2H9.17627L8.46492 4.8454C8.42758 4.99477 8.46114 5.15302 8.55589 5.27437C8.65064 5.39573 8.79602 5.46667 8.94999 5.46667H12.3725L8.0395 12.2757L8.5783 8.50404C8.5988 8.36056 8.55602 8.21523 8.46105 8.10573C8.36608 7.99623 8.22827 7.93333 8.08332 7.93333H5.03077C4.70548 7.93333 4.4668 7.62764 4.5457 7.31207L5.77903 2.37873Z" fill="currentColor"/>
</svg>
${__("Upgrade plan")}
</button>`
: ""
}
</div>
</div>`);
const banner_message = isFCUser
? "Please upgrade for uninterrupted services"
: "Please contact your system administrator to upgrade your plan.";
let card_args = {
title: `Your trial ends in ${trial_end_string}`,
message: banner_message,
outline: true,
close_button: true,
popper: true,
primary_button_alignment: "right",
dismiss_key: `${frappe.boot.site_info.name}_trial_card_time`,
dismiss_it_for: "day",
};
if (isFCUser) {
$.extend(card_args, {
primary_action_label: "Upgrade",
primary_action_suffix_icon: "square-arrow-out-up-right",
styles: {
"sidebar-card-button-bg-color": "var(--surface-gray-2)",
"sidebar-card-button-color": "var(--ink-gray-7)",
"sidebar-card-button-outline": "var(--ink-gray-7)",
},
primary_action: () => {
openFrappeCloudDashboard();
},
});
}
$(document).on("desktop_screen", function (event, data) {
if (
frappe.boot.is_fc_site &&
!!frappe.boot.setup_complete &&
!frappe.is_mobile() &&
frappe.user.has_role("System Manager")
) {
if (response.trial_end_date && trial_end_date > new Date()) {
card_args.parent = $(".icons-container").first();
let banner_card = new frappe.ui.SidebarCard(card_args);
}
addManageBillingDropdown(data.desktop);
$(".login-to-fc, .upgrade-plan-button").on("click", function () {
openFrappeCloudDashboard();
});
}
});
});
function setErrorMessage(message) {
$("#fc-login-error").text(message);
}
function addManageBillingDropdown(desktop) {
desktop.add_menu_item({
label: __("Manage Billing"),
icon: "receipt-text",
condition: function () {
return frappe.boot.is_fc_site;
},
onClick: function () {
return openFrappeCloudDashboard();
},
});
}
function openFrappeCloudDashboard() {
window.open(
`${frappeCloudBaseEndpoint}/dashboard/sites/${frappe.boot.site_info.name}`,
"_blank"
);
}

View file

@ -35,6 +35,7 @@ export class InfoCard {
trigger: $(this.label_span).find("svg").get(0),
close_button: true,
popper: true,
primary_button_width: "full",
};
if (this.df.documentation_url) {
card_args.primary_action_label = "Read More";

View file

@ -99,6 +99,7 @@ frappe.ui.Sidebar = class Sidebar {
this.workspace_sidebar_items = updated_items;
}
setup(workspace_title) {
$(document).trigger("sidebar_setup", { sidebar: this });
this.sidebar_title = workspace_title;
this.check_for_private_workspace(workspace_title);
this.workspace_title = this.sidebar_title.toLowerCase();
@ -110,11 +111,9 @@ frappe.ui.Sidebar = class Sidebar {
this.add_sidebar_cards();
}
add_card(card) {
if (
this.desktop_menu_items &&
this.desktop_menu_items.find((i) => i.to_title_case === card.title)
)
return;
if (this.cards && this.cards.find((i) => i.title === card.title)) return;
card.parent = this.wrapper.find(".body-sidebar-cards");
delete card.styles;
this.cards.push(card);
}
add_sidebar_cards() {

View file

@ -10,15 +10,16 @@
{% } else { %}
<div class="card-title-container card-close-button">
{%= frappe.utils.icon(card.icon, "sm", "", "", "card-icon") %}
<span class="flex flex-column">
<span class="flex flex-column w-100">
<div class="sidebar-card-title">{{ card.title }}</div>
<div class="sidebar-card-description">{{ card.message }}</div>
</span>
{%= frappe.utils.icon("x","sm", "", "", "card-icon cursor-pointer") %}
{%= frappe.utils.icon("x","sm", "", "", "card-icon cursor-pointer close-button") %}
</div>
{% } %}
{% if(card.primary_action_label) { %}
<button class="sidebar-card-button btn">
<div class="sidebar-card-actions flex">
{% if(card.primary_action_label) { %}
<button class="sidebar-card-button btn {%= card.primary_button_width ? 'w-100' : '' %}">
{% if (card.primary_action_icon) %}
{{ frappe.utils.icon(card.primary_action_icon, "sm", "", "", "", true) }}
{% } %}
@ -28,4 +29,5 @@
{% endif %}
</button>
{% } %}
</div>
</div>

View file

@ -1,19 +1,33 @@
import { createPopper } from "@popperjs/core";
frappe.provide("frappe.ui");
// icon, title, message, condition, primary_action_label, primary_action
frappe.ui.SidebarCard = class SidebarCard {
constructor(opts) {
Object.assign(this, opts);
this.alignment_style_map = {
right: "flex-end",
left: "flex-start",
};
this.dismiss_intervals = {
minute: 60 * 1000,
hour: 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
};
this.make(opts);
this.setup();
this.display = false;
this.set_styles();
}
make() {
if (!this.icon) {
this.icon = "info";
}
if (this.dismiss_it_for) {
const next_time_for_show = localStorage.getItem(this.get_dismiss_key());
if (next_time_for_show && Date.now() < Number(next_time_for_show)) {
this.hide();
}
}
this.card = $(
frappe.render_template("sidebar_card", {
card: this,
@ -31,10 +45,17 @@ frappe.ui.SidebarCard = class SidebarCard {
],
});
}
if (this.outline) {
this.card.addClass("card-outline");
this.card.removeClass("px-2 py-2");
}
this.card.prependTo(this.parent);
this.set_button_alignment();
this.show();
}
setup() {
this.setup_primary_action();
this.setup_close_button();
}
toggle() {
if (this.display) {
@ -46,11 +67,18 @@ frappe.ui.SidebarCard = class SidebarCard {
hide() {
this.display = false;
this.parent.removeAttr("data-show");
this.card.removeClass("d-inline-flex");
this.card.addClass("hidden");
}
show() {
this.display = true;
this.parent.attr("data-show", "");
this.popper.update();
this.popper && this.popper.update();
this.card.addClass("d-inline-flex");
this.card.removeClass("hidden");
}
get_dismiss_key() {
return this.dismiss_key || "card_next_show_time";
}
setup_primary_action() {
const me = this;
@ -59,6 +87,19 @@ frappe.ui.SidebarCard = class SidebarCard {
me.primary_action(event);
});
}
setup_close_button() {
const me = this;
if (this.close_button) {
this.card.find(".close-button").on("click", function () {
if (me.dismiss_it_for) {
let next_show_time = Date.now() + me.dismiss_intervals[me.dismiss_it_for];
localStorage.setItem(me.get_dismiss_key(), next_show_time);
}
me.toggle();
});
}
}
set_styles() {
if (this.styles) {
const $root = $(":root");
@ -67,4 +108,11 @@ frappe.ui.SidebarCard = class SidebarCard {
}
}
}
set_button_alignment() {
if (this.primary_button_alignment) {
this.card
.find(".sidebar-card-actions")
.css("justifyContent", this.alignment_style_map[this.primary_button_alignment]);
}
}
};

View file

@ -1,6 +1,6 @@
:root {
--sidebar-card-button-outline: var(--surface-blue-3);
--sidebar-card-button-bg-color: var(var(--surface-blue-2));
--sidebar-card-button-bg-color: var(--surface-blue-2);
--sidebar-card-button-color: var(--ink-blue-3);
}
.card-title-container {
@ -54,3 +54,11 @@
.cursor-pointer {
cursor: pointer;
}
.card-outline {
border: 1px solid;
box-shadow: none;
border-color: var(--outline-gray-2, #e2e2e2);
border-radius: calc(var(--border-radius-lg) + 2px);
padding: calc(var(--padding-md) - 3px);
}