diff --git a/apps/web/core/components/project/settings/features-list.tsx b/apps/web/core/components/project/settings/features-list.tsx index f4411c2b0..0c10c80fc 100644 --- a/apps/web/core/components/project/settings/features-list.tsx +++ b/apps/web/core/components/project/settings/features-list.tsx @@ -8,7 +8,6 @@ import { observer } from "mobx-react"; // plane imports import { useTranslation } from "@plane/i18n"; import { setPromiseToast } from "@plane/propel/toast"; -import { Tooltip } from "@makeplane/propel/components/tooltip"; import type { IProject } from "@plane/types"; import { CyclesOutline, IntakeOutline, ModuleOutline, PagesOutline, ViewsOutline } from "@makeplane/propel/icons"; // components @@ -16,8 +15,6 @@ import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-it import { SettingsHeading } from "@/components/settings/heading"; // hooks import { useProject } from "@/hooks/store/use-project"; -// plane web imports -import { UpgradeBadge } from "@/components/workspace/upgrade-badge"; // local imports import { ProjectFeatureToggle } from "./helper"; @@ -116,16 +113,7 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props: {Object.entries(PROJECT_FEATURES_LIST).map(([featureItemKey, featureItem]) => (
- {t(featureItem.key)} - {featureItem.isPro && ( - - - - )} - - } + title={t(featureItem.key)} description={t(`${featureItem.key}_description`)} control={
    - {highlights.map((highlight, index) => ( -
  • {highlight}
  • + {highlights.map((highlight) => ( +
  • {highlight}
  • ))}
@@ -79,15 +79,15 @@ export const PlansComparisonBase = observer(function PlansComparisonBase(props: {/* Feature Comparison */} {isCompareAllFeaturesSectionOpen && ( <> - {planComparison.map((section, sectionIdx) => ( -
+ {planComparison.map((section) => ( +

{section.title} {section.comingSoon && }

- {section.features.map((feature, featureIdx) => ( + {section.features.map((feature) => (
diff --git a/apps/web/core/components/workspace/billing/comparison/plan-detail.tsx b/apps/web/core/components/workspace/billing/comparison/plan-detail.tsx index 27d2b8cf1..b43ac1da1 100644 --- a/apps/web/core/components/workspace/billing/comparison/plan-detail.tsx +++ b/apps/web/core/components/workspace/billing/comparison/plan-detail.tsx @@ -6,12 +6,7 @@ import { observer } from "mobx-react"; // plane imports -import { - SUBSCRIPTION_REDIRECTION_URLS, - SUBSCRIPTION_WITH_BILLING_FREQUENCY, - TALK_TO_SALES_URL, -} from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; +import { SUBSCRIPTION_WITH_BILLING_FREQUENCY } from "@plane/constants"; import { Button } from "@plane/propel/button"; import type { TBillingFrequency } from "@plane/types"; import { EProductSubscriptionEnum } from "@plane/types"; @@ -31,27 +26,23 @@ type TPlanDetailProps = { export const PlanDetail = observer(function PlanDetail(props: TPlanDetailProps) { const { subscriptionType, planDetail, billingFrequency, setBillingFrequency } = props; - // plane hooks - const { t } = useTranslation(); // subscription details const subscriptionName = getSubscriptionName(subscriptionType); const isSubscriptionActive = planDetail.isActive; // pricing details + const isEnterprise = subscriptionType === EProductSubscriptionEnum.ENTERPRISE; const displayPrice = billingFrequency === "month" ? planDetail.monthlyPrice : planDetail.yearlyPrice; - const pricingDescription = isSubscriptionActive ? "a user per month" : "Quote on request"; - const pricingSecondaryDescription = - billingFrequency === "month" + const pricingDescription = isEnterprise + ? "Active Lifetime License" + : isSubscriptionActive + ? "a user per month" + : "Quote on request"; + const pricingSecondaryDescription = isEnterprise + ? "All features unlocked" + : billingFrequency === "month" ? planDetail.monthlyPriceSecondaryDescription : planDetail.yearlyPriceSecondaryDescription; - const handleRedirection = () => { - const frequency = billingFrequency ?? "year"; - // Get the redirection URL based on the subscription type and billing frequency - const redirectUrl = SUBSCRIPTION_REDIRECTION_URLS[subscriptionType][frequency] ?? TALK_TO_SALES_URL; - // Open the URL in a new tab - window.open(redirectUrl, "_blank"); - }; - return (
{/* Plan name and pricing section */} @@ -100,9 +91,15 @@ export const PlanDetail = observer(function PlanDetail(props: TPlanDetailProps) {/* Subscription button */}
- + {isEnterprise ? ( + + ) : ( + + )}
); diff --git a/apps/web/core/components/workspace/billing/comparison/plans.tsx b/apps/web/core/components/workspace/billing/comparison/plans.tsx index 3273aff2d..7590a0cf1 100644 --- a/apps/web/core/components/workspace/billing/comparison/plans.tsx +++ b/apps/web/core/components/workspace/billing/comparison/plans.tsx @@ -1294,10 +1294,10 @@ export const PLANE_PLANS: PlanePlans = { enterprise: { id: EProductSubscriptionEnum.ENTERPRISE, name: "Enterprise", - monthlyPriceSecondaryDescription: "billed monthly", - yearlyPriceSecondaryDescription: "billed yearly", - buttonCTA: "Talk to Sales", - isActive: false, + monthlyPriceSecondaryDescription: "Active Plan", + yearlyPriceSecondaryDescription: "Active Plan", + buttonCTA: "Active Plan", + isActive: true, }, }, planHighlights: { diff --git a/apps/web/core/components/workspace/billing/root.tsx b/apps/web/core/components/workspace/billing/root.tsx index 33100a437..d5bdc2812 100644 --- a/apps/web/core/components/workspace/billing/root.tsx +++ b/apps/web/core/components/workspace/billing/root.tsx @@ -52,8 +52,8 @@ export const BillingRoot = observer(function BillingRoot() { />
diff --git a/apps/web/core/components/workspace/edition-badge.tsx b/apps/web/core/components/workspace/edition-badge.tsx index b4bebe7a3..1aea0464e 100644 --- a/apps/web/core/components/workspace/edition-badge.tsx +++ b/apps/web/core/components/workspace/edition-badge.tsx @@ -4,7 +4,6 @@ * See the LICENSE file for details. */ -import { useState } from "react"; import { observer } from "mobx-react"; // ui import { useTranslation } from "@plane/i18n"; @@ -14,33 +13,18 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import packageJson from "package.json"; // local components import { Button } from "@plane/propel/button"; -import { PaidPlanUpgradeModal } from "@/components/license/modal/upgrade-modal"; export const WorkspaceEditionBadge = observer(function WorkspaceEditionBadge() { - // states - const [isPaidPlanPurchaseModalOpen, setIsPaidPlanPurchaseModalOpen] = useState(false); // translation const { t } = useTranslation(); // platform const { isMobile } = usePlatformOS(); return ( - <> - setIsPaidPlanPurchaseModalOpen(false)} - /> - - - - + + + ); }); diff --git a/apps/web/core/components/workspace/upgrade-badge.tsx b/apps/web/core/components/workspace/upgrade-badge.tsx index 5bddd913d..304fa04f7 100644 --- a/apps/web/core/components/workspace/upgrade-badge.tsx +++ b/apps/web/core/components/workspace/upgrade-badge.tsx @@ -4,32 +4,11 @@ * See the LICENSE file for details. */ -// helpers -import { useTranslation } from "@plane/i18n"; -import { cn } from "@plane/utils"; - type TUpgradeBadge = { className?: string; size?: "sm" | "md"; }; -export function UpgradeBadge(props: TUpgradeBadge) { - const { className, size = "sm" } = props; - - const { t } = useTranslation(); - - return ( -
- {t("sidebar.pro")} -
- ); +export function UpgradeBadge(_props: TUpgradeBadge) { + return null; } diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 559964878..85dceb2eb 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -57,6 +57,10 @@ services: web: image: makeplane/plane-frontend:${APP_RELEASE:-stable} restart: unless-stopped + volumes: + - ./patches/page-BEQX0L4U.js:/usr/share/nginx/html/assets/page-BEQX0L4U.js:ro + - ./patches/layout-C0Zt7niA.js:/usr/share/nginx/html/assets/layout-C0Zt7niA.js:ro + - ./patches/upgrade-badge-BxAQzHXC.js:/usr/share/nginx/html/assets/upgrade-badge-BxAQzHXC.js:ro depends_on: - api - worker diff --git a/deploy/install.sh b/deploy/install.sh index 992432257..82ce55e38 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -44,6 +44,11 @@ docker compose pull web space admin live api worker beat-worker migrator plane-r echo "==> [4/6] Executing database migrations..." docker compose run --rm migrator +echo "==> Generating frontend Enterprise patches..." +if [ -f "${INSTALL_DIR}/patches/patch_frontend.py" ]; then + python3 "${INSTALL_DIR}/patches/patch_frontend.py" "${INSTALL_DIR}/patches" +fi + echo "==> [5/6] Starting Plane containers without touching other host services..." docker compose up -d --remove-orphans diff --git a/deploy/patches/patch_frontend.py b/deploy/patches/patch_frontend.py new file mode 100644 index 000000000..614da1805 --- /dev/null +++ b/deploy/patches/patch_frontend.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import os +import subprocess +import sys +import urllib.request + +def extract_or_fetch(filename, out_path, image="makeplane/plane-frontend:stable"): + src_container_path = f"/usr/share/nginx/html/assets/{filename}" + try: + res = subprocess.run( + ["docker", "run", "--rm", image, "cat", src_container_path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + if len(res.stdout) > 50: + with open(out_path, "wb") as f: + f.write(res.stdout) + print(f"Extracted {filename} from {image} ({len(res.stdout)} bytes)") + return + except Exception as e: + print(f"Docker extraction failed ({e}), falling back to HTTP fetch...") + + url = f"https://jira.tradhox.com/assets/{filename}" + urllib.request.urlretrieve(url, out_path) + print(f"Fetched {filename} from {url}") + +def patch_page(file_path): + with open(file_path, "r", encoding="utf-8") as f: + code = f.read() + + # 1. Replace Community with Enterprise in top box + old_box = "(0,L.jsx)(A,{title:`Community`,description:`Unlimited projects, issues, cycles, modules, pages, and storage`})" + new_box = "(0,L.jsx)(A,{title:`Enterprise`,description:`Enterprise Edition Active — Unlimited users, projects, issues, cycles, modules, pages, storage, and all enterprise features unlocked`})" + if old_box in code: + code = code.replace(old_box, new_box) + + # 2. Update W for enterprise + old_ent = "enterprise:{id:o.ENTERPRISE,name:`Enterprise`,monthlyPriceSecondaryDescription:`billed monthly`,yearlyPriceSecondaryDescription:`billed yearly`,buttonCTA:`Talk to Sales`,isActive:!1}" + new_ent = "enterprise:{id:o.ENTERPRISE,name:`Enterprise`,monthlyPriceSecondaryDescription:`Active Plan`,yearlyPriceSecondaryDescription:`Active Plan`,buttonCTA:`Active Plan`,isActive:!0}" + if old_ent in code: + code = code.replace(old_ent, new_ent) + + # 3. Update Y pricing descriptions and button + old_ph = "p=d?`a user per month`:`Quote on request`,h=r===`month`?n.monthlyPriceSecondaryDescription:n.yearlyPriceSecondaryDescription;" + new_ph = "p=t===o.ENTERPRISE?`Active Lifetime License`:d?`a user per month`:`Quote on request`,h=t===o.ENTERPRISE?`All features unlocked`:r===`month`?n.monthlyPriceSecondaryDescription:n.yearlyPriceSecondaryDescription;" + if old_ph in code: + code = code.replace(old_ph, new_ph) + + old_btn = "(0,L.jsx)(T,{variant:`primary`,size:`lg`,onClick:()=>{let e=r??`year`,n=l[t][e]??`https://plane.so/talk-to-sales`;window.open(n,`_blank`)},className:`w-full`,children:d?`Upgrade to ${u}`:a(`common.upgrade_cta.talk_to_sales`)})" + new_btn = "(0,L.jsx)(T,{variant:t===o.ENTERPRISE?`primary`:`secondary`,size:`lg`,disabled:!0,className:t===o.ENTERPRISE?`w-full opacity-95 cursor-default`:`w-full opacity-60 cursor-default`,children:t===o.ENTERPRISE?`Current Plan (Active)`:`Included in Enterprise`})" + if old_btn in code: + code = code.replace(old_btn, new_btn) + + with open(file_path, "w", encoding="utf-8") as f: + f.write(code) + print(f"Patched page successfully: {file_path} ({len(code)} bytes)") + +def patch_layout(file_path): + with open(file_path, "r", encoding="utf-8") as f: + code = f.read() + + old_badge = 'onClick:()=>t(!0),"aria-haspopup":`dialog`,"aria-label":n(`aria_labels.projects_sidebar.edition_badge`),children:`Community`' + new_badge = 'onClick:()=>{},"aria-label":n(`aria_labels.projects_sidebar.edition_badge`),children:`Enterprise`' + if old_badge in code: + code = code.replace(old_badge, new_badge) + + with open(file_path, "w", encoding="utf-8") as f: + f.write(code) + print(f"Patched layout successfully: {file_path} ({len(code)} bytes)") + +def patch_badge(file_path): + # The upgrade badge returns null so no "Pro" badges render anywhere + new_code = 'import{t as e}from"./jsx-runtime-noaePsda.js";var r=e();function i(e){return null;}export{i as t};\n' + with open(file_path, "w", encoding="utf-8") as f: + f.write(new_code) + print(f"Patched badge successfully: {file_path} ({len(new_code)} bytes)") + +def main(): + target_dir = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)) + image = sys.argv[2] if len(sys.argv) > 2 else "makeplane/plane-frontend:stable" + os.makedirs(target_dir, exist_ok=True) + + page_file = os.path.join(target_dir, "page-BEQX0L4U.js") + layout_file = os.path.join(target_dir, "layout-C0Zt7niA.js") + badge_file = os.path.join(target_dir, "upgrade-badge-BxAQzHXC.js") + + extract_or_fetch("page-BEQX0L4U.js", page_file, image) + patch_page(page_file) + + extract_or_fetch("layout-C0Zt7niA.js", layout_file, image) + patch_layout(layout_file) + + extract_or_fetch("upgrade-badge-BxAQzHXC.js", badge_file, image) + patch_badge(badge_file) + + print("All frontend chunks successfully patched!") + +if __name__ == "__main__": + main()