mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 14:01:45 +02:00
@@ -13,7 +13,6 @@ from plane.ee.models import WorkspaceLicense
|
||||
|
||||
|
||||
def fetch_workspace_license(workspace_id, workspace_slug, free_seats=12):
|
||||
|
||||
# If the number of free seats is less than 12, set it to 12
|
||||
workspace_free_seats = 12 if free_seats <= 12 else free_seats
|
||||
owner_email = Workspace.objects.get(slug=workspace_slug).owner.email
|
||||
@@ -54,6 +53,89 @@ def fetch_workspace_license(workspace_id, workspace_slug, free_seats=12):
|
||||
return response
|
||||
|
||||
|
||||
def is_trial_allowed(workspace_license):
|
||||
"""The free trial is only allowed if the workspace has not activated the free trial and the workspace does not have a subscription"""
|
||||
if (
|
||||
not workspace_license.subscription
|
||||
and not workspace_license.has_activated_free_trial
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_upgraded(workspace_license):
|
||||
"""Check if the workspace has upgraded from the trial"""
|
||||
if (
|
||||
workspace_license.subscription
|
||||
and workspace_license.has_added_payment_method
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_on_trial(workspace_license):
|
||||
"Check if the workspace is on a trial"
|
||||
if (
|
||||
workspace_license.subscription
|
||||
and not has_upgraded(workspace_license)
|
||||
and workspace_license.trial_end_date
|
||||
and workspace_license.trial_end_date >= timezone.now()
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def trial_remaining_days(workspace_license):
|
||||
"""Calculate the remaining days of the trial"""
|
||||
if (
|
||||
workspace_license.subscription
|
||||
and not has_upgraded(workspace_license)
|
||||
and workspace_license.trial_end_date
|
||||
and workspace_license.trial_end_date >= timezone.now()
|
||||
):
|
||||
return (workspace_license.trial_end_date - timezone.now()).days
|
||||
return None
|
||||
|
||||
|
||||
def is_billing_active(workspace_license):
|
||||
"""Check if the billing is active"""
|
||||
|
||||
if workspace_license.plan == WorkspaceLicense.PlanChoice.FREE:
|
||||
return False
|
||||
|
||||
if (
|
||||
workspace_license.plan == WorkspaceLicense.PlanChoice.PRO
|
||||
and is_on_trial(workspace_license)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def show_payment_button(workspace_license):
|
||||
"""Check if the workspace is allowed to show the payment button"""
|
||||
|
||||
# If the workspace is on free product then show the payment button
|
||||
if workspace_license.plan == WorkspaceLicense.PlanChoice.FREE:
|
||||
return True
|
||||
# If the workspace is on pro product and is on trial then show the payment button
|
||||
if (
|
||||
workspace_license.plan == WorkspaceLicense.PlanChoice.PRO
|
||||
and is_on_trial(workspace_license)
|
||||
and not has_upgraded(workspace_license)
|
||||
):
|
||||
return True
|
||||
# If the workspace is on pro product and has not upgraded then show the payment button
|
||||
return False
|
||||
|
||||
|
||||
def show_trial_banner(workspace_license):
|
||||
"""Determine if the trial banner should be shown"""
|
||||
if is_on_trial(workspace_license) and not has_upgraded(workspace_license):
|
||||
return trial_remaining_days(workspace_license) <= 4
|
||||
return False
|
||||
|
||||
|
||||
def resync_workspace_license(workspace_slug, force=False):
|
||||
# Fetch the workspace
|
||||
workspace = Workspace.objects.get(slug=workspace_slug)
|
||||
@@ -69,7 +151,6 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
if (
|
||||
workspace_license.last_synced_at - timezone.now()
|
||||
).total_seconds() > 3600 or force:
|
||||
|
||||
# Fetch the workspace license
|
||||
response = fetch_workspace_license(
|
||||
workspace_id=str(workspace.id),
|
||||
@@ -106,6 +187,10 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
)
|
||||
workspace_license.subscription = response.get("subscription")
|
||||
workspace_license.save()
|
||||
|
||||
workspace_license = WorkspaceLicense.objects.get(
|
||||
workspace=workspace
|
||||
)
|
||||
return {
|
||||
"is_cancelled": workspace_license.is_cancelled,
|
||||
"purchased_seats": workspace_license.purchased_seats,
|
||||
@@ -119,6 +204,14 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
"subscription": workspace_license.subscription,
|
||||
"is_self_managed": os.environ.get("IS_MULTI_TENANT", "0")
|
||||
== "0",
|
||||
"is_on_trial": is_on_trial(workspace_license),
|
||||
"is_trial_allowed": is_trial_allowed(workspace_license),
|
||||
"remaining_trial_days": trial_remaining_days(
|
||||
workspace_license
|
||||
),
|
||||
"has_upgraded": has_upgraded(workspace_license),
|
||||
"show_payment_button": show_payment_button(workspace_license),
|
||||
"show_trial_banner": show_trial_banner(workspace_license),
|
||||
}
|
||||
else:
|
||||
return {
|
||||
@@ -134,6 +227,14 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
"subscription": workspace_license.subscription,
|
||||
"is_self_managed": os.environ.get("IS_MULTI_TENANT", "0")
|
||||
== "0",
|
||||
"is_on_trial": is_on_trial(workspace_license),
|
||||
"is_trial_allowed": is_trial_allowed(workspace_license),
|
||||
"remaining_trial_days": trial_remaining_days(
|
||||
workspace_license
|
||||
),
|
||||
"has_upgraded": has_upgraded(workspace_license),
|
||||
"show_payment_button": show_payment_button(workspace_license),
|
||||
"show_trial_banner": show_trial_banner(workspace_license),
|
||||
}
|
||||
# If the license is not present, then fetch the license from the payment server and create it
|
||||
else:
|
||||
@@ -148,7 +249,7 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
).count(),
|
||||
)
|
||||
# Create the workspace license
|
||||
workspace_license = WorkspaceLicense.objects.create(
|
||||
_ = WorkspaceLicense.objects.create(
|
||||
workspace=workspace,
|
||||
is_cancelled=response.get("is_cancelled", False),
|
||||
purchased_seats=response.get("purchased_seats", 0),
|
||||
@@ -166,6 +267,8 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
),
|
||||
subscription=response.get("subscription"),
|
||||
)
|
||||
|
||||
workspace_license = WorkspaceLicense.objects.get(workspace=workspace)
|
||||
# Return the workspace license
|
||||
return {
|
||||
"is_cancelled": workspace_license.is_cancelled,
|
||||
@@ -179,4 +282,10 @@ def resync_workspace_license(workspace_slug, force=False):
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
"subscription": workspace_license.subscription,
|
||||
"is_self_managed": os.environ.get("IS_MULTI_TENANT", "0") == "0",
|
||||
"is_on_trial": is_on_trial(workspace_license),
|
||||
"is_trial_allowed": is_trial_allowed(workspace_license),
|
||||
"remaining_trial_days": trial_remaining_days(workspace_license),
|
||||
"has_upgraded": has_upgraded(workspace_license),
|
||||
"show_payment_button": show_payment_button(workspace_license),
|
||||
"show_trial_banner": show_trial_banner(workspace_license),
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ from plane.app.permissions.workspace import WorkspaceOwnerPermission
|
||||
from plane.db.models import WorkspaceMember, Workspace
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.payment.utils.workspace_license_request import (
|
||||
resync_workspace_license,
|
||||
)
|
||||
|
||||
|
||||
class PaymentLinkEndpoint(BaseAPIView):
|
||||
@@ -54,25 +57,53 @@ class PaymentLinkEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
if settings.PAYMENT_SERVER_BASE_URL:
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/payment-link/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={
|
||||
"workspace_id": str(workspace.id),
|
||||
"slug": slug,
|
||||
"stripe_product_id": product_id,
|
||||
"stripe_price_id": price_id,
|
||||
"customer_email": request.user.email,
|
||||
"members_list": list(workspace_members),
|
||||
"host": base_host(request=request, is_app=True),
|
||||
},
|
||||
# Fetch the workspace license
|
||||
workspace_license_response = resync_workspace_license(
|
||||
workspace_slug=slug
|
||||
)
|
||||
response.raise_for_status()
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
# Check if the workspace is on trial
|
||||
if workspace_license_response.get("is_on_trial"):
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/trial-subscriptions/upgrade/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={
|
||||
"workspace_id": str(workspace.id),
|
||||
"stripe_price_id": price_id,
|
||||
"members_list": list(workspace_members),
|
||||
"slug": slug,
|
||||
},
|
||||
)
|
||||
# Check if the response is successful
|
||||
response.raise_for_status()
|
||||
# Convert the response to json
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
# Check if the workspace is on a paid plan
|
||||
else:
|
||||
# Create the payment link
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/payment-link/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={
|
||||
"workspace_id": str(workspace.id),
|
||||
"slug": slug,
|
||||
"stripe_product_id": product_id,
|
||||
"stripe_price_id": price_id,
|
||||
"customer_email": request.user.email,
|
||||
"members_list": list(workspace_members),
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
# Convert the response to json
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "error fetching payment link"},
|
||||
|
||||
@@ -21,6 +21,8 @@ from plane.ee.models import WorkspaceLicense
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.payment.utils.workspace_license_request import (
|
||||
resync_workspace_license,
|
||||
is_billing_active,
|
||||
is_on_trial,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,36 +95,55 @@ class WebsiteUserWorkspaceEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
# Get all the workspaces where the user is admin
|
||||
workspace_ids = WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role=20,
|
||||
).values_list("workspace_id", flat=True)
|
||||
|
||||
# Fetch the workspaces from the workspace license
|
||||
workspace_licenses = (
|
||||
WorkspaceLicense.objects.filter(workspace_id__in=workspace_ids)
|
||||
.annotate(slug=F("workspace__slug"))
|
||||
.annotate(name=F("workspace__name"))
|
||||
.annotate(logo=F("workspace__logo"))
|
||||
.annotate(product=F("plan"))
|
||||
workspaces = (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role=20,
|
||||
)
|
||||
.annotate(
|
||||
slug=F("workspace__slug"),
|
||||
logo=F("workspace__logo"),
|
||||
name=F("workspace__name"),
|
||||
)
|
||||
.values(
|
||||
"workspace_id",
|
||||
"slug",
|
||||
"name",
|
||||
"logo",
|
||||
"product",
|
||||
"trial_end_date",
|
||||
"has_activated_free_trial",
|
||||
"has_added_payment_method",
|
||||
"current_period_end_date",
|
||||
"is_offline_payment",
|
||||
"subscription",
|
||||
)
|
||||
)
|
||||
|
||||
workspace_ids = [workspace["workspace_id"] for workspace in workspaces]
|
||||
|
||||
|
||||
# Fetch the workspaces from the workspace license
|
||||
workspace_licenses = WorkspaceLicense.objects.filter(
|
||||
workspace_id__in=workspace_ids
|
||||
)
|
||||
|
||||
for workspace in workspaces:
|
||||
licenses = [
|
||||
license
|
||||
for license in workspace_licenses
|
||||
if str(license.workspace_id) == str(workspace["workspace_id"])
|
||||
]
|
||||
|
||||
if licenses:
|
||||
workspace["product"] = licenses[0].plan
|
||||
workspace["is_on_trial"] = is_on_trial(
|
||||
licenses[0]
|
||||
)
|
||||
workspace["is_billing_active"] = is_billing_active(
|
||||
licenses[0]
|
||||
)
|
||||
else:
|
||||
workspace["product"] = "FREE"
|
||||
workspace["is_on_trial"] = False
|
||||
workspace["is_billing_active"] = False
|
||||
|
||||
# Get the workspace details
|
||||
return Response(workspace_licenses, status=status.HTTP_200_OK)
|
||||
return Response(workspaces, status=status.HTTP_200_OK)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
if e.response.status_code == 400:
|
||||
|
||||
9
packages/types/src/payment.d.ts
vendored
9
packages/types/src/payment.d.ts
vendored
@@ -14,12 +14,13 @@ export type IPaymentProduct = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: Omit<TProductSubscriptionType, "FREE">;
|
||||
payment_quantity: number;
|
||||
prices: IPaymentProductPrice[];
|
||||
};
|
||||
|
||||
export type IWorkspaceProductSubscription = {
|
||||
product: TProductSubscriptionType;
|
||||
is_canceled?: boolean;
|
||||
is_cancelled?: boolean;
|
||||
is_self_managed: boolean;
|
||||
interval?: "month" | "year" | null;
|
||||
current_period_end_date: string | null;
|
||||
@@ -29,4 +30,10 @@ export type IWorkspaceProductSubscription = {
|
||||
has_activated_free_trial: boolean;
|
||||
has_added_payment_method: boolean;
|
||||
subscription: string | undefined;
|
||||
is_on_trial: boolean;
|
||||
is_trial_allowed: boolean;
|
||||
remaining_trial_days: number | null;
|
||||
has_upgraded: boolean;
|
||||
show_payment_button: boolean;
|
||||
show_trial_banner: boolean;
|
||||
};
|
||||
|
||||
@@ -12,11 +12,9 @@ import { Button, Loader, setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { PaymentService } from "@/plane-web/services/payment.service";
|
||||
|
||||
const paymentService = new PaymentService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
const calculateYearlyDiscount = (monthlyPrice: number, yearlyPricePerMonth: number): number => {
|
||||
const monthlyCost = monthlyPrice * 12;
|
||||
@@ -39,13 +37,6 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
|
||||
const { selectedWorkspace: workspaceSlug } = useParams();
|
||||
|
||||
// fetch workspace members
|
||||
const { data: workspaceMembers, isLoading: isFetching } = useSWR(
|
||||
workspaceSlug ? `CLOUD_PRO_WORKSPACE_MEMBER_DETAILS` : null,
|
||||
workspaceSlug ? () => workspaceService.fetchWorkspaceMembers(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch products
|
||||
const { data: products, isLoading: isLoadingProduct } = useSWR(
|
||||
workspaceSlug ? `CLOUD_PRO_PRODUCTS_${workspaceSlug?.toString()}` : null,
|
||||
@@ -57,27 +48,16 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
// fetch workspace current plan
|
||||
const { data: workspaceSubscription, isLoading: workspaceSubscriptionLoading } = useSWR(
|
||||
workspaceSlug ? `CLOUD_PRO_WORKSPACE_SUBSCRIPTION_PLAN_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => paymentService.getWorkspaceCurrentPlan(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// derived values
|
||||
const totalWorkspaceMembers = (workspaceMembers || [])?.filter((member) => member.role >= 15)?.length;
|
||||
const proProduct = (products || [])?.find((product: IPaymentProduct) => product?.type === "PRO");
|
||||
|
||||
const monthlyPlan = proProduct?.prices?.find((price) => price.recurring === "month");
|
||||
const yearlyPlan = proProduct?.prices?.find((price) => price.recurring === "year");
|
||||
const totalPaidMembers = proProduct?.payment_quantity ?? 1;
|
||||
|
||||
const monthlyPlanUnitPrice = (monthlyPlan?.unit_amount || 0) / 100;
|
||||
const yearlyPlanUnitPrice = (yearlyPlan?.unit_amount || 0) / 1200;
|
||||
const yearlyDiscountedPrice = calculateYearlyDiscount(monthlyPlanUnitPrice, yearlyPlanUnitPrice);
|
||||
|
||||
const workspaceOnTrial =
|
||||
workspaceSubscription?.has_activated_free_trial && workspaceSubscription?.trial_end_date ? true : false;
|
||||
|
||||
const handleStripeCheckout = (priceId: string) => {
|
||||
if (!workspaceSlug) {
|
||||
setToast({
|
||||
@@ -93,38 +73,8 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
product_id: proProduct?.id,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.payment_link) {
|
||||
window.open(response.payment_link, "_self");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.detail ?? "Failed to generate payment link. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTrialStripeCheckout = (priceId: string) => {
|
||||
if (!workspaceSlug) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Please select a workspace to continue",
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
paymentService
|
||||
.modifyTrailSubscription(workspaceSlug.toString(), {
|
||||
price_id: priceId,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.session_url) {
|
||||
window.open(response.session_url, "_self");
|
||||
if (response.url) {
|
||||
window.open(response.url, "_self");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -143,12 +93,12 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-2">
|
||||
<div className="flex flex-col items-center gap-2 pb-4">
|
||||
<div className="text-3xl font-semibold">Choose your billing frequency</div>
|
||||
{yearlyDiscountedPrice && (
|
||||
{!!yearlyDiscountedPrice && (
|
||||
<div className="text-center text-base text-custom-text-300">{`Upgrade to our yearly plan and get ${Math.round(yearlyDiscountedPrice)}% off.`}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFetching || isLoadingProduct || workspaceSubscriptionLoading ? (
|
||||
{isLoadingProduct ? (
|
||||
<div className="flex flex-col rounded gap-1 w-full">
|
||||
<Loader.Item height="90px" />
|
||||
<Loader.Item height="90px" />
|
||||
@@ -177,13 +127,13 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
{plan.recurring === selectedPlan?.recurring && <Check className="size-4 stroke-2" />}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
{totalWorkspaceMembers && (
|
||||
{totalPaidMembers && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl font-semibold leading-7">
|
||||
${renderPlanPricing(plan.unit_amount, 1, plan.recurring)}
|
||||
</span>
|
||||
<span className="text-sm text-custom-text-300">
|
||||
{` per user per ${plan.recurring} x ${totalWorkspaceMembers}`}
|
||||
{` per user per ${plan.recurring} x ${totalPaidMembers}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -193,9 +143,9 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col text-right">
|
||||
{totalWorkspaceMembers && (
|
||||
{totalPaidMembers && (
|
||||
<span className="text-2xl font-semibold leading-7">
|
||||
${renderPlanPricing(plan.unit_amount, totalWorkspaceMembers, plan.recurring)}
|
||||
${renderPlanPricing(plan.unit_amount, totalPaidMembers, plan.recurring)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-custom-text-300">
|
||||
@@ -209,15 +159,10 @@ const CloudUpgradePlanPage = observer(() => {
|
||||
|
||||
{selectedPlan?.recurring && selectedPlan.unit_amount ? (
|
||||
<>
|
||||
<Button
|
||||
className="w-full px-2 my-4"
|
||||
onClick={() =>
|
||||
workspaceOnTrial ? handleTrialStripeCheckout(selectedPlan.id) : handleStripeCheckout(selectedPlan.id)
|
||||
}
|
||||
>
|
||||
<Button className="w-full px-2 my-4" onClick={() => handleStripeCheckout(selectedPlan.id)}>
|
||||
{isLoading
|
||||
? "Redirecting to Stripe..."
|
||||
: `Pay $${renderPlanPricing(selectedPlan.unit_amount, totalWorkspaceMembers, selectedPlan.recurring)} every ${selectedPlan?.recurring}`}
|
||||
: `Pay $${renderPlanPricing(selectedPlan.unit_amount, totalPaidMembers, selectedPlan.recurring)} every ${selectedPlan?.recurring}`}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -72,16 +72,11 @@ const CloudUpgradePage = observer(() => {
|
||||
};
|
||||
|
||||
const workspaceSubscription = (workspace: TWorkspaceWithProductDetails): "FREE" | "PRO" | "TRIAL" => {
|
||||
if (!workspace.subscription) return "FREE";
|
||||
switch (workspace.product) {
|
||||
case "PRO":
|
||||
if (workspace.is_offline_payment) {
|
||||
return "PRO";
|
||||
} else if (workspace.trial_end_date && workspace.has_added_payment_method) {
|
||||
return "PRO";
|
||||
} else if (workspace.trial_end_date && !workspace.has_added_payment_method) {
|
||||
if (workspace.is_on_trial) {
|
||||
return "TRIAL";
|
||||
}
|
||||
} else return "PRO";
|
||||
case "FREE":
|
||||
return "FREE";
|
||||
default:
|
||||
@@ -95,7 +90,7 @@ const CloudUpgradePage = observer(() => {
|
||||
name="workspace-upgrade-radio-input"
|
||||
label={
|
||||
<div className="flex flex-col items-center gap-2 pb-4">
|
||||
<div className="text-3xl font-semibold">
|
||||
<div className="text-center text-3xl font-semibold">
|
||||
{isAnyWorkspaceAvailable
|
||||
? "Choose your workspace"
|
||||
: "We didn't find an eligible workspace for this upgrade. Try another email address."}
|
||||
@@ -164,7 +159,7 @@ const CloudUpgradePage = observer(() => {
|
||||
</div>
|
||||
),
|
||||
value: workspace.slug,
|
||||
disabled: ["PRO"].includes(workspaceSubscription(workspace)),
|
||||
disabled: workspace.is_billing_active,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -49,97 +49,46 @@ export const CloudEditionBadge = observer(() => {
|
||||
if (!subscriptionDetail) return null;
|
||||
|
||||
const renderButtonText = () => {
|
||||
if (!subscriptionDetail.subscription) {
|
||||
return "Upgrade to Pro";
|
||||
}
|
||||
switch (subscriptionDetail.product) {
|
||||
case "FREE": {
|
||||
if (subscriptionDetail?.has_activated_free_trial) {
|
||||
return "Buy Pro";
|
||||
} else {
|
||||
return "Upgrade to Pro";
|
||||
}
|
||||
}
|
||||
case "PRO":
|
||||
if (subscriptionDetail?.has_activated_free_trial && subscriptionDetail?.has_added_payment_method) {
|
||||
return "Purchased Pro";
|
||||
} else {
|
||||
if (subscriptionDetail?.has_activated_free_trial && !subscriptionDetail?.has_added_payment_method) {
|
||||
return "Buy Pro";
|
||||
} else {
|
||||
return "Upgrade to Pro";
|
||||
}
|
||||
}
|
||||
default:
|
||||
case "FREE":
|
||||
return "Upgrade to Pro";
|
||||
}
|
||||
};
|
||||
|
||||
const showPaymentButton = () => {
|
||||
if (!subscriptionDetail.subscription) {
|
||||
return true;
|
||||
}
|
||||
switch (subscriptionDetail.product) {
|
||||
case "FREE": {
|
||||
return true;
|
||||
}
|
||||
case "PRO":
|
||||
if (subscriptionDetail.is_offline_payment) {
|
||||
return false;
|
||||
} else if (subscriptionDetail?.has_activated_free_trial && subscriptionDetail?.has_added_payment_method) {
|
||||
return false;
|
||||
} else {
|
||||
if (subscriptionDetail?.has_activated_free_trial && !subscriptionDetail?.has_added_payment_method) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return `Pro trial ends ${subscriptionDetail.remaining_trial_days}d`;
|
||||
default:
|
||||
return false;
|
||||
return "Upgrade";
|
||||
}
|
||||
};
|
||||
|
||||
const showPlaneProButton = showPaymentButton() ? false : subscriptionDetail.product === "PRO" ? true : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProPlanCloudUpgradeModal
|
||||
isOpen={isProPlanModalOpen}
|
||||
handleClose={() => toggleProPlanModal(false)}
|
||||
yearlyPlan={false}
|
||||
handleSuccessModal={() => handleSuccessModalToggle(true)}
|
||||
/>
|
||||
|
||||
{showPaymentButton() && (
|
||||
{subscriptionDetail.show_payment_button && (
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
className="w-fit min-w-24 cursor-pointer rounded-2xl px-4 py-1 text-center text-sm font-medium outline-none"
|
||||
className="w-full cursor-pointer rounded-2xl px-4 py-1.5 text-center text-sm font-medium outline-none"
|
||||
onClick={handleProPlanPurchaseModalOpen}
|
||||
>
|
||||
{renderButtonText()}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showPlaneProButton && (
|
||||
{!subscriptionDetail.show_payment_button && (
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
className="w-fit cursor-pointer rounded-2xl px-4 py-1 text-center text-sm font-medium outline-none"
|
||||
className="w-full cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
|
||||
onClick={handlePaidPlanSuccessModalOpen}
|
||||
>
|
||||
<Image src={PlaneLogo} alt="Plane Pro" width={12} height={12} />
|
||||
<Image src={PlaneLogo} alt="Plane Pro" width={14} height={14} />
|
||||
Plane Pro
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Fallback text */}
|
||||
{!showPaymentButton() && !showPlaneProButton && (
|
||||
<div className="w-full cursor-default rounded-md bg-green-500/10 px-2 py-1 text-center text-xs font-medium text-green-500 outline-none leading-6">
|
||||
Enterprise Edition
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Button } from "@plane/ui";
|
||||
// helpers
|
||||
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
// plane web components
|
||||
@@ -12,42 +10,29 @@ import { ProPlanCloudUpgradeModal } from "@/plane-web/components/license";
|
||||
// plane web hooks
|
||||
import { useWorkspaceSubscription } from "@/plane-web/hooks/store";
|
||||
|
||||
// constants
|
||||
const MAX_DAYS_LEFT = 4;
|
||||
|
||||
export const FreeTrialBanner: FC = observer(() => {
|
||||
// hooks
|
||||
const { config } = useInstance();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspaceSubscribedPlanDetail: subscriptionDetail } = useWorkspaceSubscription();
|
||||
|
||||
// states
|
||||
const [pricingModalOpen, setPricingModalOpen] = useState(false);
|
||||
|
||||
// derived values
|
||||
const daysLeft = findHowManyDaysLeft(subscriptionDetail?.trial_end_date) || 0;
|
||||
|
||||
// validate weather to show the banner or not for the current workspace and subscription details
|
||||
if (!currentWorkspace || !subscriptionDetail || !config?.payment_server_base_url) return <></>;
|
||||
// validating for offline payment
|
||||
if (subscriptionDetail.is_offline_payment || !subscriptionDetail.trial_end_date) return <></>;
|
||||
// if the days left is more than the max days left then don't show the banner
|
||||
if (daysLeft > MAX_DAYS_LEFT || daysLeft < 0) return <></>;
|
||||
// if the trial banner is not allowed to show then don't show the banner
|
||||
if (!subscriptionDetail.show_trial_banner) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* This modal is intentionally placed inside the condition to avoid unnecessary calls to list product endpoint. */}
|
||||
<ProPlanCloudUpgradeModal
|
||||
isOpen={pricingModalOpen}
|
||||
handleClose={() => setPricingModalOpen(false)}
|
||||
yearlyPlan={false}
|
||||
/>
|
||||
<ProPlanCloudUpgradeModal isOpen={pricingModalOpen} handleClose={() => setPricingModalOpen(false)} />
|
||||
|
||||
<div className="bg-custom-primary-100/10 text-custom-primary-100 py-2 px-5">
|
||||
<div className="relative container mx-auto flex justify-center items-center gap-2">
|
||||
<div className="text-sm font-medium">
|
||||
Your free trial is ending in {daysLeft} days. When your trial ends, your workspace will downgrade to Free
|
||||
and you will lose access to
|
||||
Your free trial is ending in {subscriptionDetail.remaining_trial_days} days. When your trial ends, your
|
||||
workspace will downgrade to Free and you will lose access to
|
||||
<a
|
||||
href="https://plane.so/pro"
|
||||
target="_blank"
|
||||
|
||||
@@ -25,10 +25,9 @@ export const PlaneCloudBilling: React.FC = observer(() => {
|
||||
const { currentWorkspaceSubscribedPlanDetail, toggleProPlanModal } = useWorkspaceSubscription();
|
||||
// derived values
|
||||
const endDate = currentWorkspaceSubscribedPlanDetail?.current_period_end_date;
|
||||
const isSubscriptionCancelled = currentWorkspaceSubscribedPlanDetail?.is_canceled;
|
||||
const isSubscriptionCancelled = currentWorkspaceSubscribedPlanDetail?.is_cancelled;
|
||||
const isInTrialPeriod =
|
||||
!currentWorkspaceSubscribedPlanDetail?.has_added_payment_method &&
|
||||
!!currentWorkspaceSubscribedPlanDetail?.trial_end_date;
|
||||
currentWorkspaceSubscribedPlanDetail?.is_on_trial && !currentWorkspaceSubscribedPlanDetail.has_upgraded;
|
||||
|
||||
const handleSubscriptionPageRedirection = () => {
|
||||
setIsLoading(true);
|
||||
@@ -109,11 +108,11 @@ export const PlaneCloudBilling: React.FC = observer(() => {
|
||||
<>
|
||||
{isInTrialPeriod && (
|
||||
<>
|
||||
<div className="flex-shrink-0 p-1 px-2 uppercase bg-custom-primary-100/20 text-custom-primary-100 text-xs rounded-full font-medium">
|
||||
Free Trial
|
||||
<div className="flex-shrink-0 p-1 px-2 bg-custom-primary-100/20 text-custom-primary-100 text-xs rounded-full font-medium">
|
||||
Pro Trial
|
||||
</div>
|
||||
<div className="text-center text-sm text-custom-text-200 font-medium">
|
||||
(Free trial ends on:{" "}
|
||||
(Pro trial ends on:{" "}
|
||||
{renderFormattedDate(currentWorkspaceSubscribedPlanDetail?.trial_end_date)})
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,6 @@ const paymentService = new PaymentService();
|
||||
export type ProPlanCloudUpgradeModalProps = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
yearlyPlan?: boolean;
|
||||
handleSuccessModal?: () => void;
|
||||
canFetchProducts?: boolean;
|
||||
};
|
||||
@@ -37,7 +36,7 @@ export const calculateYearlyDiscount = (monthlyPrice: number, yearlyPricePerMont
|
||||
};
|
||||
|
||||
export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (props) => {
|
||||
const { isOpen, handleClose, yearlyPlan, handleSuccessModal, canFetchProducts = true } = props;
|
||||
const { isOpen, handleClose, handleSuccessModal, canFetchProducts = true } = props;
|
||||
// params
|
||||
const { workspaceSlug } = useParams();
|
||||
// states
|
||||
@@ -48,11 +47,8 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const {
|
||||
currentWorkspaceSubscribedPlanDetail: subscriptionDetail,
|
||||
fetchWorkspaceSubscribedPlan,
|
||||
freeTrialSubscription,
|
||||
} = useWorkspaceSubscription();
|
||||
const { currentWorkspaceSubscribedPlanDetail: subscriptionDetail, freeTrialSubscription } =
|
||||
useWorkspaceSubscription();
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
// fetch products
|
||||
@@ -75,8 +71,8 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
product_id: proProduct?.id,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.payment_link) {
|
||||
window.open(response.payment_link, "_self");
|
||||
if (response.url) {
|
||||
window.open(response.url, "_self");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -92,34 +88,6 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
});
|
||||
};
|
||||
|
||||
const handlePlanUpgrade = (priceId: string) => {
|
||||
setLoading(true);
|
||||
paymentService
|
||||
.upgradeCurrentWorkspaceSubscription(workspaceSlug.toString(), {
|
||||
price_id: priceId,
|
||||
product_id: proProduct?.id,
|
||||
})
|
||||
.then(async () => {
|
||||
await fetchWorkspaceSubscribedPlan(workspaceSlug.toString());
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Your workspace has been upgraded to Pro yearly plan.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.detail ?? "Failed to upgrade. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
// handling the payment link when the free trial is disabled
|
||||
const handlePaymentLink = (priceId: string) => {
|
||||
if (!workspaceSlug) return;
|
||||
@@ -144,44 +112,7 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
|
||||
captureEvent("pro_plan_payment_link_clicked", { workspaceSlug });
|
||||
|
||||
if (yearlyPlan) {
|
||||
handlePlanUpgrade(priceId);
|
||||
} else {
|
||||
handleStripeCheckout(priceId);
|
||||
}
|
||||
};
|
||||
|
||||
// handling the payment link when the free trial is enabled
|
||||
const handleSubscriptionPageRedirection = (priceId: string) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
if (!isAdmin) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Unauthorized!",
|
||||
message: "You don't have permission to perform this action.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
paymentService
|
||||
.modifyTrailSubscription(workspaceSlug.toString(), { price_id: priceId })
|
||||
.then((response) => {
|
||||
if (response.session_url) {
|
||||
window.open(response.session_url, "_blank");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to redirect to subscription page. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
handleStripeCheckout(priceId);
|
||||
};
|
||||
|
||||
// handle trial
|
||||
@@ -213,35 +144,26 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
(orderBy(proProduct?.prices || [], ["recurring"], ["desc"])?.find((price) => price.recurring === "year")
|
||||
?.unit_amount || 0) / 1200;
|
||||
const yearlyDiscount = calculateYearlyDiscount(monthlyPrice, yearlyPrice);
|
||||
const isInTrailPeriod =
|
||||
subscriptionDetail?.has_activated_free_trial &&
|
||||
subscriptionDetail?.subscription &&
|
||||
subscriptionDetail?.trial_end_date &&
|
||||
new Date(subscriptionDetail.trial_end_date) >= new Date();
|
||||
const isTrialCompleted =
|
||||
subscriptionDetail &&
|
||||
!subscriptionDetail.subscription &&
|
||||
subscriptionDetail.has_activated_free_trial &&
|
||||
subscriptionDetail.product === "FREE"
|
||||
? true
|
||||
: false;
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} width={EModalWidth.XXL} className="rounded-xl">
|
||||
<div className="py-6 px-10 max-h-[90vh] sm:max-h-[95vh] overflow-auto">
|
||||
{isInTrailPeriod && (
|
||||
{subscriptionDetail?.is_on_trial && (
|
||||
<div className="relative flex justify-center items-center">
|
||||
<div className="p-1 px-2 uppercase bg-custom-primary-100/20 text-custom-primary-100 text-xs rounded-full font-medium">
|
||||
Free Trial
|
||||
<div className="p-1 px-2 bg-custom-primary-100/20 text-custom-primary-100 text-xs rounded-full font-medium">
|
||||
Pro trial in progress
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Dialog.Title as="h2" className="text-2xl font-bold leading-6 mt-6 flex justify-center items-center">
|
||||
<Dialog.Title
|
||||
as="h2"
|
||||
className="text-2xl font-bold leading-6 mt-4 flex justify-center items-center text-center"
|
||||
>
|
||||
Upgrade to Pro, yearly for flat {yearlyDiscount}% off.
|
||||
</Dialog.Title>
|
||||
<div className="mt-3 mb-12">
|
||||
<div className="mt-3 mb-8">
|
||||
<p className="text-center text-sm mb-4 px-10 text-custom-text-100">
|
||||
Pro unlocks everything that teams need to make progress and move work forward.Upgrade today and get
|
||||
Pro unlocks everything that teams need to make progress and move work forward. Upgrade today and get
|
||||
{yearlyDiscount}% off on your yearly bill.
|
||||
</p>
|
||||
<p className="text-center text-sm text-custom-primary-200 font-semibold underline">
|
||||
@@ -273,16 +195,11 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
basePlan="Free"
|
||||
features={PRO_PLAN_FEATURES_MAP}
|
||||
isLoading={isLoading}
|
||||
handlePaymentLink={(priceId: string) =>
|
||||
isInTrailPeriod && !isTrialCompleted
|
||||
? handleSubscriptionPageRedirection(priceId)
|
||||
: handlePaymentLink(priceId)
|
||||
}
|
||||
yearlyPlanOnly={yearlyPlan}
|
||||
handlePaymentLink={handlePaymentLink}
|
||||
trialLoader={trialLoader}
|
||||
handleTrial={handleTrial}
|
||||
yearlyDiscount={yearlyDiscount}
|
||||
showTrialButton={!isInTrailPeriod && !isTrialCompleted}
|
||||
showTrialButton={subscriptionDetail?.is_trial_allowed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ProPlanSelfHostUpgradeModal: FC<ProPlanSelfHostUpgradeModalProps> =
|
||||
name: "Pro",
|
||||
type: "PRO",
|
||||
description: "Pro plan details for self-hosted users",
|
||||
payment_quantity: 1,
|
||||
prices: [
|
||||
{
|
||||
id: "pro_monthly",
|
||||
|
||||
@@ -13,7 +13,6 @@ export type ProPlanUpgradeProps = {
|
||||
features: { label: string; comingSoon: boolean }[];
|
||||
handlePaymentLink: (priceId: string) => void;
|
||||
isLoading?: boolean;
|
||||
yearlyPlanOnly?: boolean;
|
||||
verticalFeatureList?: boolean;
|
||||
extraFeatures?: string | React.ReactNode;
|
||||
trialLoader?: boolean;
|
||||
@@ -29,7 +28,6 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
features,
|
||||
handlePaymentLink,
|
||||
isLoading = false,
|
||||
yearlyPlanOnly = false,
|
||||
verticalFeatureList = false,
|
||||
extraFeatures,
|
||||
trialLoader = false,
|
||||
@@ -38,14 +36,7 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
showTrialButton = false,
|
||||
} = props;
|
||||
// derived values
|
||||
const yearlyPrice = orderBy(proProduct?.prices || [], ["recurring"], ["desc"]).find(
|
||||
(price) => price.recurring === "year"
|
||||
);
|
||||
const proProductPrices = yearlyPlanOnly
|
||||
? yearlyPrice
|
||||
? [yearlyPrice]
|
||||
: []
|
||||
: orderBy(proProduct?.prices || [], ["recurring"], ["asc"]);
|
||||
const proProductPrices = orderBy(proProduct?.prices || [], ["recurring"], ["asc"]);
|
||||
|
||||
const renderPricing = (unitAmount: number, recurring: string): number => {
|
||||
let price = 0;
|
||||
@@ -58,33 +49,31 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
<div className="py-4 px-2 border border-custom-primary-200/30 rounded-xl bg-custom-primary-200/5">
|
||||
<Tab.Group defaultIndex={1}>
|
||||
<div className="flex w-full justify-center">
|
||||
{!yearlyPlanOnly && (
|
||||
<Tab.List className="flex space-x-1 rounded-lg bg-custom-primary-200/10 p-1 w-60">
|
||||
{proProductPrices.map((price: IPaymentProductPrice) => (
|
||||
<Tab
|
||||
key={price?.id}
|
||||
className={({ selected }) =>
|
||||
cn(
|
||||
"w-full rounded-lg py-1.5 text-sm font-medium leading-5",
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-primary-300 shadow"
|
||||
: "hover:bg-custom-primary-100/5 text-custom-text-300 hover:text-custom-text-200"
|
||||
)
|
||||
}
|
||||
>
|
||||
<>
|
||||
{price.recurring === "month" && ("Monthly" as string)}
|
||||
{price.recurring === "year" && ("Yearly" as string)}
|
||||
{price.recurring === "year" && yearlyDiscount && (
|
||||
<span className="bg-gradient-to-r from-[#C78401] to-[#896828] text-white rounded-full px-2 py-1 ml-1 text-xs">
|
||||
-{yearlyDiscount}%
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
)}
|
||||
<Tab.List className="flex space-x-1 rounded-lg bg-custom-primary-200/10 p-1 w-60">
|
||||
{proProductPrices.map((price: IPaymentProductPrice) => (
|
||||
<Tab
|
||||
key={price?.id}
|
||||
className={({ selected }) =>
|
||||
cn(
|
||||
"w-full rounded-lg py-1.5 text-sm font-medium leading-5",
|
||||
selected
|
||||
? "bg-custom-background-100 text-custom-primary-300 shadow"
|
||||
: "hover:bg-custom-primary-100/5 text-custom-text-300 hover:text-custom-text-200"
|
||||
)
|
||||
}
|
||||
>
|
||||
<>
|
||||
{price.recurring === "month" && ("Monthly" as string)}
|
||||
{price.recurring === "year" && ("Yearly" as string)}
|
||||
{price.recurring === "year" && yearlyDiscount && (
|
||||
<span className="bg-gradient-to-r from-[#C78401] to-[#896828] text-white rounded-full px-2 py-1 ml-1 text-xs">
|
||||
-{yearlyDiscount}%
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
</div>
|
||||
<Tab.Panels>
|
||||
{proProductPrices?.map((price: IPaymentProductPrice) => (
|
||||
@@ -104,11 +93,7 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
onClick={() => handlePaymentLink(price.id)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading
|
||||
? yearlyPlanOnly
|
||||
? "Upgrading your plan..."
|
||||
: "Redirecting to Stripe..."
|
||||
: "Upgrade to Pro"}
|
||||
{isLoading ? "Redirecting to Stripe..." : "Upgrade to Pro"}
|
||||
</button>
|
||||
|
||||
{/* trail button */}
|
||||
@@ -122,12 +107,6 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
<div className="w-3 h-3">{trialLoader && <Loader size={12} className="animate-spin" />}</div>
|
||||
</button>
|
||||
)}
|
||||
{yearlyPlanOnly && (
|
||||
<div className="text-[9px] text-custom-text-300 w-64 pt-1">
|
||||
We will charge your card on file at <b>$5 per user per month</b> for the total number of users in
|
||||
your workspace and update your subscription from <b>Pro, monthly</b> or <b>Pro, yearly.</b>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-2 pt-6 pb-2">
|
||||
<div className="p-2 text-sm font-semibold">{`Everything in ${basePlan} +`}</div>
|
||||
|
||||
@@ -25,14 +25,6 @@ export class PaymentService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async upgradeCurrentWorkspaceSubscription(workspaceSlug: string, data = {}) {
|
||||
return this.post(`/api/payments/workspaces/${workspaceSlug}/subscriptions/upgrade/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceCurrentPlan(workspaceSlug: string): Promise<IWorkspaceProductSubscription> {
|
||||
return this.get(`/api/payments/workspaces/${workspaceSlug}/current-plan/`)
|
||||
.then((response) => response?.data)
|
||||
@@ -72,15 +64,4 @@ export class PaymentService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async modifyTrailSubscription(
|
||||
workspaceSlug: string,
|
||||
payload: { price_id: string }
|
||||
): Promise<{ session_url: string }> {
|
||||
return this.post(`/api/payments/workspaces/${workspaceSlug}/trial-subscriptions/upgrade/`, payload)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,17 +62,23 @@ export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore {
|
||||
const response = await paymentService.getWorkspaceCurrentPlan(workspaceSlug);
|
||||
runInAction(() => {
|
||||
set(this.subscribedPlan, workspaceSlug, {
|
||||
product: response?.product || "FREE",
|
||||
is_canceled: response?.is_canceled || false,
|
||||
is_self_managed: response?.is_self_managed || false,
|
||||
interval: response?.interval || null,
|
||||
product: response?.product ?? "FREE",
|
||||
is_cancelled: response?.is_cancelled ?? false,
|
||||
is_self_managed: response?.is_self_managed ?? false,
|
||||
interval: response?.interval ?? null,
|
||||
current_period_end_date: response?.current_period_end_date,
|
||||
is_offline_payment: response?.is_offline_payment || false,
|
||||
trial_end_date: response?.trial_end_date || undefined,
|
||||
purchased_seats: response?.purchased_seats || 0,
|
||||
has_activated_free_trial: response?.has_activated_free_trial || false,
|
||||
has_added_payment_method: response?.has_added_payment_method || false,
|
||||
subscription: response?.subscription || undefined,
|
||||
is_offline_payment: response?.is_offline_payment ?? false,
|
||||
trial_end_date: response?.trial_end_date ?? undefined,
|
||||
purchased_seats: response?.purchased_seats ?? 0,
|
||||
has_activated_free_trial: response?.has_activated_free_trial ?? false,
|
||||
has_added_payment_method: response?.has_added_payment_method ?? false,
|
||||
subscription: response?.subscription ?? undefined,
|
||||
is_on_trial: response?.is_on_trial ?? false,
|
||||
is_trial_allowed: response?.is_trial_allowed ?? false,
|
||||
remaining_trial_days: response?.remaining_trial_days ?? null,
|
||||
has_upgraded: response?.has_upgraded ?? false,
|
||||
show_payment_button: response?.show_payment_button ?? true,
|
||||
show_trial_banner: response?.show_trial_banner ?? false,
|
||||
});
|
||||
});
|
||||
return response;
|
||||
@@ -80,10 +86,11 @@ export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore {
|
||||
runInAction(() => {
|
||||
set(this.subscribedPlan, workspaceSlug, {
|
||||
product: "FREE",
|
||||
is_canceled: false,
|
||||
is_cancelled: false,
|
||||
is_self_managed: false,
|
||||
interval: null,
|
||||
current_period_end_date: null,
|
||||
show_payment_button: true,
|
||||
});
|
||||
});
|
||||
throw error;
|
||||
|
||||
8
web/ee/types/workspace.d.ts
vendored
8
web/ee/types/workspace.d.ts
vendored
@@ -7,10 +7,6 @@ export type TWorkspaceWithProductDetails = {
|
||||
name: string;
|
||||
logo: string;
|
||||
product: TProductSubscriptionType;
|
||||
current_period_end_date: string;
|
||||
has_activated_free_trial: boolean;
|
||||
has_added_payment_method: boolean;
|
||||
trial_end_date: string;
|
||||
is_offline_payment: boolean;
|
||||
subscription: string;
|
||||
is_on_trial: boolean;
|
||||
is_billing_active: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user