mirror of
https://github.com/makeplane/plane.git
synced 2026-07-14 06:25:58 +02:00
improvement: billing and plans (#1962)
* improvement: update modal UI * chore: total member count chip * chore: seats banner ux copy improvement * improvement: workspace disabled page and seats banner ux copy
This commit is contained in:
@@ -10,6 +10,7 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types";
|
||||
import { Button, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { NotAuthorizedView } from "@/components/auth-screens";
|
||||
import { CountChip } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace";
|
||||
// constants
|
||||
@@ -43,7 +44,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const {
|
||||
workspace: { inviteMembersToWorkspace },
|
||||
workspace: { workspaceMemberIds, inviteMembersToWorkspace },
|
||||
} = useMember();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
@@ -145,7 +146,12 @@ const WorkspaceMembersSettingsPage = observer(() => {
|
||||
})}
|
||||
>
|
||||
<div className="flex justify-between gap-4 pb-3.5 items-start">
|
||||
<h4 className="text-xl font-medium">Members</h4>
|
||||
<h4 className="flex items-center gap-2.5 text-xl font-medium">
|
||||
Members
|
||||
{workspaceMemberIds && workspaceMemberIds.length > 0 && (
|
||||
<CountChip count={workspaceMemberIds.length} className="h-5 m-auto" />
|
||||
)}
|
||||
</h4>
|
||||
<div className="ml-auto flex items-center gap-1.5 rounded-md border border-custom-border-200 bg-custom-background-100 px-2.5 py-1.5">
|
||||
<Search className="h-3.5 w-3.5 text-custom-text-400" />
|
||||
<input
|
||||
|
||||
@@ -39,9 +39,9 @@ export const LicenseSeatsBanner: FC = observer(() => {
|
||||
: EPlanMessageKeys.NEAR_LIMIT;
|
||||
|
||||
const planMessages = {
|
||||
free_plan: `Your workspace has been grandfathered to ${freeSeats} over the 12-member limit on the Free plan. To add more members, upgrade to Pro.`,
|
||||
near_limit: `You’ve already ${totalSeats} out of ${freeSeats} members allowed on the Free plan. You can only add ${freeSeats - totalSeats} more members. To remove the limit, upgrade to Pro.`,
|
||||
limit_reached: `You’ve reached the members limit of ${freeSeats} on this plan. To add more members, upgrade to Pro`,
|
||||
free_plan: `Your workspace has been grandfathered to ${freeSeats} over the 12-user limit on the Free plan. To add more users, upgrade to a paid plan.`,
|
||||
near_limit: `You have ${totalSeats} out of ${freeSeats} users allowed on the Free plan. To remove the limit, upgrade to a paid plan.`,
|
||||
limit_reached: `You have ${totalSeats} out of ${freeSeats} users allowed on the Free plan. To remove the limit, upgrade to a paid plan.`,
|
||||
};
|
||||
const currentVariant: "primary" | "danger" =
|
||||
currentPlanKey && [EPlanMessageKeys.NEAR_LIMIT, EPlanMessageKeys.LIMIT_REACHED].includes(currentPlanKey)
|
||||
|
||||
@@ -204,12 +204,15 @@ export const PlanUpgradeCard: FC<PlanUpgradeCardProps> = (props) => {
|
||||
? "Redirecting to Stripe..."
|
||||
: (upgradeCTA ?? `Upgrade to ${planeName}`)}
|
||||
</button>
|
||||
{isTrialAllowed &&
|
||||
renderTrialButton &&
|
||||
renderTrialButton({
|
||||
productId: product?.id,
|
||||
priceId: price.id,
|
||||
})}
|
||||
{isTrialAllowed && (
|
||||
<div className="mt-4 h-4">
|
||||
{renderTrialButton &&
|
||||
renderTrialButton({
|
||||
productId: product?.id,
|
||||
priceId: price.id,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-2 pt-6 pb-2">
|
||||
|
||||
@@ -37,6 +37,7 @@ export const TalkToSalesCard: FC<TalkToSalesCardProps> = (props) => {
|
||||
const { currentWorkspaceSubscribedPlanDetail: subscriptionDetail } = useWorkspaceSubscription();
|
||||
// derived values
|
||||
const isSelfHosted = !!subscriptionDetail?.is_self_managed;
|
||||
const isTrialAllowed = !!subscriptionDetail?.is_trial_allowed;
|
||||
const basePlan = getBasePlanName(planVariant, isSelfHosted);
|
||||
// Plane details
|
||||
const planeName = planVariant.charAt(0).toUpperCase() + planVariant.slice(1).toLowerCase();
|
||||
@@ -73,6 +74,7 @@ export const TalkToSalesCard: FC<TalkToSalesCardProps> = (props) => {
|
||||
>
|
||||
Talk to sales
|
||||
</a>
|
||||
{isTrialAllowed && <div className="mt-4 h-4" />}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-2 pt-6 pb-2">
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ProTrialButton: React.FC<TTrialButtonProps> = (props: TTrialButtonP
|
||||
return (
|
||||
<button
|
||||
disabled={trialLoader}
|
||||
className="mt-4 text-center text-sm text-custom-text-300 hover:text-custom-text-100 font-medium transition-all flex justify-center items-center gap-2"
|
||||
className="text-center text-sm text-custom-text-300 hover:text-custom-text-100 font-medium transition-all flex justify-center items-center gap-2"
|
||||
onClick={() => handleTrial(productId, priceId)}
|
||||
>
|
||||
<span>Start free trial</span>
|
||||
|
||||
@@ -9,8 +9,6 @@ import { BUSINESS_PLAN_FEATURES, PRO_PLAN_FEATURES } from "@plane/constants";
|
||||
import { IPaymentProduct, TProductSubscriptionType } from "@plane/types";
|
||||
// ui
|
||||
import { EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// ce components
|
||||
import { OnePlanUpgrade } from "@/ce/components/workspace/upgrade";
|
||||
// plane web components
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { ProTrialButton } from "@/plane-web/components/license/modal/trial-button";
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useState } from "react";
|
||||
import { AppSidebar } from "app/[workspaceSlug]/(projects)/sidebar";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
// hooks
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
import { AlertModalCore, Button } from "@plane/ui";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// plane web components
|
||||
import { PaidPlanUpgradeModal } from "@/plane-web/components/license";
|
||||
// plane web constants
|
||||
@@ -21,9 +21,11 @@ import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
|
||||
|
||||
export const WorkspaceDisabledPage: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug: routerWorkspaceSlug } = useParams();
|
||||
// state
|
||||
const [isPaidPlanModalOpen, togglePaidPlanModal] = useState(false);
|
||||
const [isDowngradeModalOpen, toggleDowngradeModal] = useState(false);
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
@@ -31,8 +33,26 @@ export const WorkspaceDisabledPage: React.FC = observer(() => {
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug);
|
||||
|
||||
const handleRemoveMembers = () => {
|
||||
toggleDowngradeModal(false);
|
||||
router.push(`/${workspaceSlug}/settings/members`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModalCore
|
||||
variant="danger"
|
||||
isOpen={isDowngradeModalOpen}
|
||||
title="You've more than 12 users in this workspace."
|
||||
primaryButtonText={{
|
||||
loading: "Redirecting",
|
||||
default: "Remove members",
|
||||
}}
|
||||
content={<>The Free tier only lets you have 12 users. Remove users to continue using the Free tier. </>}
|
||||
handleClose={() => toggleDowngradeModal(false)}
|
||||
handleSubmit={handleRemoveMembers}
|
||||
isSubmitting={false}
|
||||
/>
|
||||
<PaidPlanUpgradeModal isOpen={isPaidPlanModalOpen} handleClose={() => togglePaidPlanModal(false)} />
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
<AppSidebar />
|
||||
@@ -51,21 +71,21 @@ export const WorkspaceDisabledPage: React.FC = observer(() => {
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-medium py-2.5">
|
||||
{isWorkspaceAdmin ? "Your payment has failed!" : "Something’s not okay"}
|
||||
{isWorkspaceAdmin ? "We couldn't collect your last invoiced payment." : "A payment is overdue."}
|
||||
</p>
|
||||
<p className="text-custom-text-300">
|
||||
{isWorkspaceAdmin
|
||||
? "We tried to bill your account but the payment method is facing issues. Please fix the issue to resume service."
|
||||
: "Your workspace is facing some issues. Please contact your admin."}
|
||||
? "You have a payment due for your Pro workspace. Please clear it to continue using Pro features or downgrade to Free."
|
||||
: "Your have a payment due for your Pro workspace. Get in touch with one of your admins to fix this."}
|
||||
</p>
|
||||
</div>
|
||||
{isWorkspaceAdmin && (
|
||||
<div className="flex items-center justify-center gap-4 pt-8">
|
||||
<Link href={`/${workspaceSlug}/settings/members`} className={getButtonStyling("link-neutral", "md")}>
|
||||
Remove members
|
||||
</Link>
|
||||
<Button size="md" onClick={() => togglePaidPlanModal(true)}>
|
||||
Upgrade plan
|
||||
Continue subscription
|
||||
</Button>
|
||||
<Button size="md" variant="link-neutral" onClick={() => toggleDowngradeModal(true)}>
|
||||
Downgrade
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user