snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useState, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TickCircleOutline } from "@makeplane/propel/icons";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// plane imports
|
||||
// helpers
|
||||
import type { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types";
|
||||
import { cn, getBaseSubscriptionName, getSubscriptionName } from "@plane/utils";
|
||||
|
||||
export type TBasePaidPlanCardProps = {
|
||||
planVariant: EProductSubscriptionEnum;
|
||||
features: string[];
|
||||
prices: TSubscriptionPrice[];
|
||||
upgradeLoaderType: Omit<EProductSubscriptionEnum, "FREE"> | undefined;
|
||||
verticalFeatureList?: boolean;
|
||||
extraFeatures?: string | React.ReactNode;
|
||||
renderPriceContent: (price: TSubscriptionPrice) => React.ReactNode;
|
||||
renderActionButton: (price: TSubscriptionPrice) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBasePaidPlanCardProps) {
|
||||
const {
|
||||
planVariant,
|
||||
features,
|
||||
prices,
|
||||
verticalFeatureList = false,
|
||||
extraFeatures,
|
||||
renderPriceContent,
|
||||
renderActionButton,
|
||||
} = props;
|
||||
// states
|
||||
const [selectedPlan, setSelectedPlan] = useState<TBillingFrequency>("month");
|
||||
const basePlan = getBaseSubscriptionName(planVariant);
|
||||
// Plane details
|
||||
const planeName = getSubscriptionName(planVariant);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-xl border border-subtle bg-layer-2 px-3 py-6">
|
||||
<Tab.Group as={Fragment} selectedIndex={selectedPlan === "month" ? 0 : 1}>
|
||||
<div className="flex h-9 w-full justify-center">
|
||||
<Tab.List className="flex w-60 space-x-1 rounded-md bg-layer-3 p-0.5">
|
||||
{prices.map((price: TSubscriptionPrice) => (
|
||||
<Tab
|
||||
key={price.key}
|
||||
className={({ selected }) =>
|
||||
cn(
|
||||
"w-full rounded-sm py-1 text-caption-md-medium leading-5",
|
||||
selected
|
||||
? "border border-subtle-1 bg-layer-2 text-primary shadow-raised-100"
|
||||
: "text-tertiary hover:text-secondary"
|
||||
)
|
||||
}
|
||||
onClick={() => setSelectedPlan(price.recurring)}
|
||||
>
|
||||
{renderPriceContent(price)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels>
|
||||
{prices.map((price: TSubscriptionPrice) => (
|
||||
<Tab.Panel key={price.key}>
|
||||
<div className="pt-6 text-center">
|
||||
<div className="text-h4-medium">Plane {planeName}</div>
|
||||
{renderActionButton(price)}
|
||||
</div>
|
||||
<div className="px-2 pt-6 pb-2">
|
||||
<div className="p-2 text-caption-md-semibold">{`Everything in ${basePlan} +`}</div>
|
||||
<ul className="grid grid-cols-12 gap-x-4">
|
||||
{features.map((feature) => (
|
||||
<li
|
||||
key={feature}
|
||||
className={cn("relative col-span-12 flex rounded-md p-2", {
|
||||
"sm:col-span-6": !verticalFeatureList,
|
||||
})}
|
||||
>
|
||||
<p className="line-clamp-1 flex w-full items-center text-caption-md-medium leading-5">
|
||||
<TickCircleOutline className="mr-2 size-4 flex-shrink-0 text-tertiary" />
|
||||
<span className="truncate text-secondary">{feature}</span>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{extraFeatures && <div>{extraFeatures}</div>}
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
106
apps/web/core/components/license/modal/card/checkout-button.tsx
Normal file
106
apps/web/core/components/license/modal/card/checkout-button.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { EProductSubscriptionEnum, IPaymentProduct, TSubscriptionPrice } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
// local imports
|
||||
import { DiscountInfo } from "./discount-info";
|
||||
|
||||
export type TCheckoutParams = {
|
||||
planVariant: EProductSubscriptionEnum;
|
||||
productId: string;
|
||||
priceId: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
planeName: string;
|
||||
planVariant: EProductSubscriptionEnum;
|
||||
isLoading?: boolean;
|
||||
product: IPaymentProduct | undefined;
|
||||
price: TSubscriptionPrice;
|
||||
upgradeCTA?: string;
|
||||
upgradeLoaderType: Omit<EProductSubscriptionEnum, "FREE"> | undefined;
|
||||
renderTrialButton?: (props: { productId: string | undefined; priceId: string | undefined }) => React.ReactNode;
|
||||
handleCheckout: (params: TCheckoutParams) => void;
|
||||
isSelfHosted: boolean;
|
||||
isTrialAllowed: boolean;
|
||||
};
|
||||
|
||||
export const PlanCheckoutButton = observer(function PlanCheckoutButton(props: Props) {
|
||||
const {
|
||||
planeName,
|
||||
planVariant,
|
||||
isLoading,
|
||||
product,
|
||||
price,
|
||||
upgradeCTA,
|
||||
upgradeLoaderType,
|
||||
renderTrialButton,
|
||||
handleCheckout,
|
||||
isSelfHosted,
|
||||
isTrialAllowed,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-4 text-center">
|
||||
<div className="h-9 text-20 font-semibold">
|
||||
{isLoading ? (
|
||||
<Loader className="flex flex-col items-center justify-center">
|
||||
<Loader.Item height="36px" width="4rem" />
|
||||
</Loader>
|
||||
) : (
|
||||
<DiscountInfo
|
||||
currency={price.currency}
|
||||
frequency={price.recurring}
|
||||
price={price.price}
|
||||
subscriptionType={planVariant}
|
||||
className="mr-1.5"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-caption-md-medium text-tertiary">per user per month</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Loader className="flex flex-col items-center justify-center">
|
||||
<Loader.Item height="38px" width="14rem" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center space-y-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="w-56"
|
||||
onClick={() => {
|
||||
if (product && price.id) {
|
||||
handleCheckout({
|
||||
planVariant,
|
||||
productId: product.id,
|
||||
priceId: price.id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={!!upgradeLoaderType}
|
||||
>
|
||||
{upgradeLoaderType === planVariant ? "Redirecting to Stripe" : (upgradeCTA ?? `Upgrade to ${planeName}`)}
|
||||
</Button>
|
||||
{isTrialAllowed && !isSelfHosted && (
|
||||
<div className="mt-1 h-3">
|
||||
{renderTrialButton &&
|
||||
renderTrialButton({
|
||||
productId: product?.id,
|
||||
priceId: price.id,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import type { TBillingFrequency } from "@plane/types";
|
||||
import { EProductSubscriptionEnum } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
import ScribbleBlack from "@/app/assets/scribble/scribble-black.svg?url";
|
||||
import ScribbleWhite from "@/app/assets/scribble/scribble-white.svg?url";
|
||||
|
||||
type TDiscountInfoProps = {
|
||||
className?: string;
|
||||
currency: string;
|
||||
frequency: TBillingFrequency;
|
||||
price: number;
|
||||
subscriptionType: EProductSubscriptionEnum;
|
||||
};
|
||||
|
||||
const PLANS_WITH_DISCOUNT = [EProductSubscriptionEnum.PRO];
|
||||
|
||||
const getActualPrice = (frequency: TBillingFrequency, subscriptionType: EProductSubscriptionEnum): number | null => {
|
||||
switch (subscriptionType) {
|
||||
case EProductSubscriptionEnum.PRO:
|
||||
return frequency === "month" ? 10 : 8;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export function DiscountInfo({ className, currency, frequency, price, subscriptionType }: TDiscountInfoProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
// derived values
|
||||
const actualPrice = getActualPrice(frequency, subscriptionType);
|
||||
|
||||
if (!PLANS_WITH_DISCOUNT.includes(subscriptionType)) {
|
||||
return (
|
||||
<>
|
||||
{currency}
|
||||
{price}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{actualPrice != price && (
|
||||
<span className={cn("relative", className)}>
|
||||
<img
|
||||
src={resolvedTheme === "dark" ? ScribbleWhite : ScribbleBlack}
|
||||
alt="scribble"
|
||||
className="absolute top-1/2 left-1/2 w-full -translate-x-1/2 -translate-y-1/2 scale-x-125"
|
||||
/>
|
||||
{currency}
|
||||
{actualPrice}
|
||||
</span>
|
||||
)}
|
||||
{currency}
|
||||
{price}
|
||||
</>
|
||||
);
|
||||
}
|
||||
47
apps/web/core/components/license/modal/card/free-plan.tsx
Normal file
47
apps/web/core/components/license/modal/card/free-plan.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseCircleOutline } from "@makeplane/propel/icons";
|
||||
// plane constants
|
||||
import { FREE_PLAN_UPGRADE_FEATURES } from "@plane/constants";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type FreePlanCardProps = {
|
||||
isOnFreePlan: boolean;
|
||||
};
|
||||
|
||||
export const FreePlanCard = observer(function FreePlanCard(props: FreePlanCardProps) {
|
||||
const { isOnFreePlan } = props;
|
||||
return (
|
||||
<div className="rounded-xl bg-layer-1 px-2 py-4">
|
||||
{isOnFreePlan && (
|
||||
<div className="px-3 py-2">
|
||||
<span className="rounded-sm border border-subtle-1 bg-layer-2 px-2 py-1 text-caption-md-medium text-tertiary">
|
||||
Your plan
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-4 py-2 font-semibold">
|
||||
<div className="text-20">Free</div>
|
||||
<div className="text-caption-md text-tertiary">$0 per user per month</div>
|
||||
</div>
|
||||
<div className="px-2 pt-2 pb-3">
|
||||
<ul className="grid w-full grid-cols-12 gap-x-4">
|
||||
{FREE_PLAN_UPGRADE_FEATURES.map((feature) => (
|
||||
<li key={feature} className={cn("relative col-span-12 flex rounded-md p-2")}>
|
||||
<p className="flex w-full items-center text-caption-md-medium leading-5">
|
||||
<CloseCircleOutline className="mr-2 size-4 flex-shrink-0 text-danger-secondary" />
|
||||
<span className="truncate text-secondary">{feature}</span>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
10
apps/web/core/components/license/modal/card/index.ts
Normal file
10
apps/web/core/components/license/modal/card/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./base-paid-plan-card";
|
||||
export * from "./free-plan";
|
||||
export * from "./talk-to-sales";
|
||||
export * from "./plan-upgrade";
|
||||
115
apps/web/core/components/license/modal/card/plan-upgrade.tsx
Normal file
115
apps/web/core/components/license/modal/card/plan-upgrade.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { TALK_TO_SALES_URL } from "@plane/constants";
|
||||
import type { EProductSubscriptionEnum, IPaymentProduct, TSubscriptionPrice } from "@plane/types";
|
||||
import { calculateYearlyDiscount, getSubscriptionName, getSubscriptionPriceDetails } from "@plane/utils";
|
||||
// components
|
||||
import { BasePaidPlanCard, TalkToSalesCard } from "@/components/license";
|
||||
// local components
|
||||
import type { TCheckoutParams } from "./checkout-button";
|
||||
import { PlanCheckoutButton } from "./checkout-button";
|
||||
|
||||
export type PlanUpgradeCardProps = {
|
||||
planVariant: EProductSubscriptionEnum;
|
||||
isLoading?: boolean;
|
||||
product: IPaymentProduct | undefined;
|
||||
features: string[];
|
||||
upgradeCTA?: string;
|
||||
upgradeLoaderType?: Omit<EProductSubscriptionEnum, "FREE"> | undefined;
|
||||
verticalFeatureList?: boolean;
|
||||
extraFeatures?: string | React.ReactNode;
|
||||
renderTrialButton?: (props: { productId: string | undefined; priceId: string | undefined }) => React.ReactNode;
|
||||
handleCheckout: (params: TCheckoutParams) => void;
|
||||
isSelfHosted: boolean;
|
||||
isTrialAllowed: boolean;
|
||||
};
|
||||
|
||||
export const PlanUpgradeCard = observer(function PlanUpgradeCard(props: PlanUpgradeCardProps) {
|
||||
const {
|
||||
planVariant,
|
||||
features,
|
||||
isLoading,
|
||||
product,
|
||||
upgradeCTA,
|
||||
verticalFeatureList = false,
|
||||
extraFeatures,
|
||||
upgradeLoaderType,
|
||||
renderTrialButton,
|
||||
handleCheckout,
|
||||
isSelfHosted,
|
||||
isTrialAllowed,
|
||||
} = props;
|
||||
// price details
|
||||
const planeName = getSubscriptionName(planVariant);
|
||||
const { monthlyPriceDetails, yearlyPriceDetails } = getSubscriptionPriceDetails(product);
|
||||
const yearlyDiscount = calculateYearlyDiscount(monthlyPriceDetails.price, yearlyPriceDetails.price);
|
||||
const prices = [monthlyPriceDetails, yearlyPriceDetails];
|
||||
|
||||
if (!product?.is_active) {
|
||||
return (
|
||||
<TalkToSalesCard
|
||||
planVariant={planVariant}
|
||||
href={TALK_TO_SALES_URL}
|
||||
isLoading={isLoading}
|
||||
features={features}
|
||||
product={product}
|
||||
prices={prices}
|
||||
upgradeLoaderType={upgradeLoaderType}
|
||||
verticalFeatureList={verticalFeatureList}
|
||||
extraFeatures={extraFeatures}
|
||||
isSelfHosted={isSelfHosted}
|
||||
isTrialAllowed={isTrialAllowed}
|
||||
renderTrialButton={renderTrialButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderPriceContent = (price: TSubscriptionPrice) => (
|
||||
<>
|
||||
{price.recurring === "month" && "Monthly"}
|
||||
{price.recurring === "year" && (
|
||||
<>
|
||||
Yearly
|
||||
{yearlyDiscount > 0 && (
|
||||
<span className="text-caption-sm ml-1 rounded-full bg-accent-primary px-1.5 py-0.5 text-on-color">
|
||||
-{yearlyDiscount}%
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BasePaidPlanCard
|
||||
planVariant={planVariant}
|
||||
features={features}
|
||||
prices={prices}
|
||||
upgradeLoaderType={upgradeLoaderType}
|
||||
verticalFeatureList={verticalFeatureList}
|
||||
extraFeatures={extraFeatures}
|
||||
renderPriceContent={renderPriceContent}
|
||||
renderActionButton={(price) => (
|
||||
<PlanCheckoutButton
|
||||
planeName={planeName}
|
||||
planVariant={planVariant}
|
||||
isLoading={isLoading}
|
||||
product={product}
|
||||
price={price}
|
||||
upgradeCTA={upgradeCTA}
|
||||
upgradeLoaderType={upgradeLoaderType}
|
||||
renderTrialButton={renderTrialButton}
|
||||
handleCheckout={handleCheckout}
|
||||
isSelfHosted={isSelfHosted}
|
||||
isTrialAllowed={isTrialAllowed}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
104
apps/web/core/components/license/modal/card/talk-to-sales.tsx
Normal file
104
apps/web/core/components/license/modal/card/talk-to-sales.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// types
|
||||
// plane imports
|
||||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import type { EProductSubscriptionEnum, IPaymentProduct, TSubscriptionPrice } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// local imports
|
||||
import { BasePaidPlanCard } from "./base-paid-plan-card";
|
||||
|
||||
export type TalkToSalesCardProps = {
|
||||
planVariant: EProductSubscriptionEnum;
|
||||
href: string;
|
||||
isLoading?: boolean;
|
||||
features: string[];
|
||||
product: IPaymentProduct | undefined;
|
||||
prices: TSubscriptionPrice[];
|
||||
upgradeLoaderType: Omit<EProductSubscriptionEnum, "FREE"> | undefined;
|
||||
verticalFeatureList?: boolean;
|
||||
extraFeatures?: string | React.ReactNode;
|
||||
isSelfHosted: boolean;
|
||||
isTrialAllowed: boolean;
|
||||
renderTrialButton?: (props: { productId: string | undefined; priceId: string | undefined }) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const TalkToSalesCard = observer(function TalkToSalesCard(props: TalkToSalesCardProps) {
|
||||
const {
|
||||
planVariant,
|
||||
href,
|
||||
features,
|
||||
product,
|
||||
prices,
|
||||
isLoading,
|
||||
verticalFeatureList = false,
|
||||
extraFeatures,
|
||||
upgradeLoaderType,
|
||||
isSelfHosted,
|
||||
isTrialAllowed,
|
||||
renderTrialButton,
|
||||
} = props;
|
||||
|
||||
const renderPriceContent = (price: TSubscriptionPrice) => (
|
||||
<>
|
||||
{price.recurring === "month" && "Monthly"}
|
||||
{price.recurring === "year" && "Yearly"}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderActionButton = (price: TSubscriptionPrice) => (
|
||||
<>
|
||||
<div className="pb-4 text-center">
|
||||
<div className="flex h-9 items-center justify-center text-20 font-semibold">
|
||||
{isLoading ? (
|
||||
<Loader className="flex flex-col items-center justify-center">
|
||||
<Loader.Item height="36px" width="4rem" />
|
||||
</Loader>
|
||||
) : (
|
||||
<>Quote on request</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-caption-md-medium text-tertiary">per user per month</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Loader className="flex flex-col items-center justify-center">
|
||||
<Loader.Item height="38px" width="14rem" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<a href={href} target="_blank" className={cn(getButtonStyling("primary", "lg"), "w-56")} rel="noreferrer">
|
||||
Talk to Sales
|
||||
</a>
|
||||
{isTrialAllowed && !isSelfHosted && (
|
||||
<div className="mt-4 h-4">
|
||||
{renderTrialButton &&
|
||||
renderTrialButton({
|
||||
productId: product?.id,
|
||||
priceId: price.id,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BasePaidPlanCard
|
||||
planVariant={planVariant}
|
||||
features={features}
|
||||
prices={prices}
|
||||
upgradeLoaderType={upgradeLoaderType}
|
||||
verticalFeatureList={verticalFeatureList}
|
||||
extraFeatures={extraFeatures}
|
||||
renderPriceContent={renderPriceContent}
|
||||
renderActionButton={renderActionButton}
|
||||
/>
|
||||
);
|
||||
});
|
||||
7
apps/web/core/components/license/modal/index.ts
Normal file
7
apps/web/core/components/license/modal/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export * from "./card";
|
||||
136
apps/web/core/components/license/modal/upgrade-modal.tsx
Normal file
136
apps/web/core/components/license/modal/upgrade-modal.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import {
|
||||
BUSINESS_PLAN_FEATURES,
|
||||
ENTERPRISE_PLAN_FEATURES,
|
||||
PLANE_COMMUNITY_PRODUCTS,
|
||||
PRO_PLAN_FEATURES,
|
||||
SUBSCRIPTION_REDIRECTION_URLS,
|
||||
SUBSCRIPTION_WEBPAGE_URLS,
|
||||
TALK_TO_SALES_URL,
|
||||
} from "@plane/constants";
|
||||
import { EProductSubscriptionEnum } from "@plane/types";
|
||||
import { EModalWidth, ModalCore } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { FreePlanCard, PlanUpgradeCard } from "@/components/license";
|
||||
import type { TCheckoutParams } from "@/components/license/modal/card/checkout-button";
|
||||
|
||||
// Constants
|
||||
const COMMON_CARD_CLASSNAME = "flex flex-col w-full h-full justify-end col-span-12 sm:col-span-6 xl:col-span-3";
|
||||
const COMMON_EXTRA_FEATURES_CLASSNAME = "pt-2 text-center text-caption-md-medium text-accent-primary hover:underline";
|
||||
|
||||
export type PaidPlanUpgradeModalProps = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
export const PaidPlanUpgradeModal = observer(function PaidPlanUpgradeModal(props: PaidPlanUpgradeModalProps) {
|
||||
const { isOpen, handleClose } = props;
|
||||
// derived values
|
||||
const isSelfHosted = true;
|
||||
const isTrialAllowed = false;
|
||||
|
||||
const handleRedirection = ({ planVariant, priceId }: TCheckoutParams) => {
|
||||
// Get the product and price using plane community constants
|
||||
const product = PLANE_COMMUNITY_PRODUCTS[planVariant];
|
||||
// oxlint-disable-next-line no-shadow
|
||||
const price = product.prices.find((price) => price.id === priceId);
|
||||
const frequency = price?.recurring ?? "year";
|
||||
// Redirect to the appropriate URL
|
||||
const redirectUrl = SUBSCRIPTION_REDIRECTION_URLS[planVariant][frequency] ?? TALK_TO_SALES_URL;
|
||||
window.open(redirectUrl, "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} width={EModalWidth.VIIXL} className="rounded-2xl">
|
||||
<div className="max-h-[90vh] overflow-auto p-10">
|
||||
<div className="grid h-full grid-cols-12 gap-6">
|
||||
{/* Free Plan Section */}
|
||||
<div className={cn(COMMON_CARD_CLASSNAME)}>
|
||||
<div className="flex text-24 leading-8 font-bold">Upgrade to a paid plan and unlock missing features.</div>
|
||||
<div className="mt-4 mb-2">
|
||||
<p className="mb-4 pr-8 text-13 text-primary">
|
||||
Dashboards, Workflows, Approvals, Time Management, and other superpowers are just a click away. Upgrade
|
||||
today to unlock features your teams need yesterday.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Free plan details */}
|
||||
<FreePlanCard isOnFreePlan />
|
||||
</div>
|
||||
|
||||
{/* Pro plan */}
|
||||
<div className={cn(COMMON_CARD_CLASSNAME)}>
|
||||
<PlanUpgradeCard
|
||||
planVariant={EProductSubscriptionEnum.PRO}
|
||||
product={PLANE_COMMUNITY_PRODUCTS[EProductSubscriptionEnum.PRO]}
|
||||
features={PRO_PLAN_FEATURES}
|
||||
verticalFeatureList
|
||||
extraFeatures={
|
||||
<p className={COMMON_EXTRA_FEATURES_CLASSNAME}>
|
||||
<a href={SUBSCRIPTION_WEBPAGE_URLS[EProductSubscriptionEnum.PRO]} target="_blank" rel="noreferrer">
|
||||
See full features list
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
handleCheckout={handleRedirection}
|
||||
isSelfHosted={!!isSelfHosted}
|
||||
isTrialAllowed={!!isTrialAllowed}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(COMMON_CARD_CLASSNAME)}>
|
||||
<PlanUpgradeCard
|
||||
planVariant={EProductSubscriptionEnum.BUSINESS}
|
||||
product={PLANE_COMMUNITY_PRODUCTS[EProductSubscriptionEnum.BUSINESS]}
|
||||
features={BUSINESS_PLAN_FEATURES}
|
||||
verticalFeatureList
|
||||
extraFeatures={
|
||||
<p className={COMMON_EXTRA_FEATURES_CLASSNAME}>
|
||||
<a
|
||||
href={SUBSCRIPTION_WEBPAGE_URLS[EProductSubscriptionEnum.BUSINESS]}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
See full features list
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
handleCheckout={handleRedirection}
|
||||
isSelfHosted={!!isSelfHosted}
|
||||
isTrialAllowed={!!isTrialAllowed}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(COMMON_CARD_CLASSNAME)}>
|
||||
<PlanUpgradeCard
|
||||
planVariant={EProductSubscriptionEnum.ENTERPRISE}
|
||||
product={PLANE_COMMUNITY_PRODUCTS[EProductSubscriptionEnum.ENTERPRISE]}
|
||||
features={ENTERPRISE_PLAN_FEATURES}
|
||||
verticalFeatureList
|
||||
extraFeatures={
|
||||
<p className={COMMON_EXTRA_FEATURES_CLASSNAME}>
|
||||
<a
|
||||
href={SUBSCRIPTION_WEBPAGE_URLS[EProductSubscriptionEnum.ENTERPRISE]}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
See full features list
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
handleCheckout={handleRedirection}
|
||||
isSelfHosted={!!isSelfHosted}
|
||||
isTrialAllowed={!!isTrialAllowed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user