mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 21:40:18 +02:00
Chore workspace license trial (#834)
* chore: workspace trial and payment method subscription * dev: updated trial validation * dev: payment button validation * chore: updated typo * chore: removed logs --------- Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.14 on 2024-08-14 10:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("ee", "0004_workspacelicense_trial_end_date"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="workspacelicense",
|
||||
name="has_activated_free_trial",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="workspacelicense",
|
||||
name="has_added_payment_method",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -51,6 +51,10 @@ class WorkspaceLicense(BaseModel):
|
||||
last_synced_at = models.DateTimeField(default=timezone.now)
|
||||
# trial end date
|
||||
trial_end_date = models.DateTimeField(null=True, blank=True)
|
||||
# has activated free trial
|
||||
has_activated_free_trial = models.BooleanField(default=False)
|
||||
# is payment method added
|
||||
has_added_payment_method = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Workspace License"
|
||||
|
||||
@@ -102,6 +102,12 @@ def member_sync_task(slug):
|
||||
workspace_license.trial_end_date = updated_workspace_license[
|
||||
"trial_end_date"
|
||||
]
|
||||
workspace_license.has_activated_free_trial = (
|
||||
updated_workspace_license["has_activated_free_trial"]
|
||||
)
|
||||
workspace_license.has_added_payment_method = (
|
||||
updated_workspace_license["has_added_payment_method"]
|
||||
)
|
||||
workspace_license.last_synced_at = timezone.now()
|
||||
workspace_license.save()
|
||||
|
||||
@@ -120,6 +126,12 @@ def member_sync_task(slug):
|
||||
"is_offline_payment"
|
||||
],
|
||||
last_synced_at=timezone.now(),
|
||||
has_activated_free_trial=updated_workspace_license[
|
||||
"has_activated_free_trial"
|
||||
],
|
||||
has_added_payment_method=updated_workspace_license[
|
||||
"has_added_payment_method"
|
||||
],
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -204,6 +204,15 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
workspace_license.is_offline_payment = response.get(
|
||||
"is_offline_payment", False
|
||||
)
|
||||
workspace_license.trial_end_date = response.get(
|
||||
"trial_end_date"
|
||||
)
|
||||
workspace_license.has_activated_free_trial = (
|
||||
response.get("has_activated_free_trial", False)
|
||||
)
|
||||
workspace_license.has_added_payment_method = (
|
||||
response.get("has_added_payment_method", False)
|
||||
)
|
||||
workspace_license.save()
|
||||
|
||||
return Response(
|
||||
@@ -215,6 +224,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
"product": workspace_license.plan,
|
||||
"is_offline_payment": workspace_license.is_offline_payment,
|
||||
"trial_end_date": workspace_license.trial_end_date,
|
||||
"has_activated_free_trial": workspace_license.has_activated_free_trial,
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -228,6 +239,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
"product": workspace_license.plan,
|
||||
"is_offline_payment": workspace_license.is_offline_payment,
|
||||
"trial_end_date": workspace_license.trial_end_date,
|
||||
"has_activated_free_trial": workspace_license.has_activated_free_trial,
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -256,6 +269,12 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
plan=response.get("plan"),
|
||||
last_synced_at=timezone.now(),
|
||||
trial_end_date=response.get("trial_end_date"),
|
||||
has_activated_free_trial=response.get(
|
||||
"has_activated_free_trial", False
|
||||
),
|
||||
has_added_payment_method=response.get(
|
||||
"has_added_payment_method", False
|
||||
),
|
||||
)
|
||||
# Return the workspace license
|
||||
return Response(
|
||||
@@ -267,6 +286,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
"product": workspace_license.plan,
|
||||
"is_offline_payment": workspace_license.is_offline_payment,
|
||||
"trial_end_date": workspace_license.trial_end_date,
|
||||
"has_activated_free_trial": workspace_license.has_activated_free_trial,
|
||||
"has_added_payment_method": workspace_license.has_added_payment_method,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -318,6 +339,12 @@ class WorkspaceLicenseRefreshEndpoint(BaseAPIView):
|
||||
workspace_license.recurring_interval = response.get("interval")
|
||||
workspace_license.plan = response.get("plan")
|
||||
workspace_license.trial_end_date = response.get("trial_end_date")
|
||||
workspace_license.has_activated_free_trial = response.get(
|
||||
"has_activated_free_trial", False
|
||||
)
|
||||
workspace_license.has_added_payment_method = response.get(
|
||||
"has_added_payment_method", False
|
||||
)
|
||||
workspace_license.last_synced_at = timezone.now()
|
||||
workspace_license.save()
|
||||
# If the license is not present, then fetch the license from the payment server and create it
|
||||
@@ -345,6 +372,12 @@ class WorkspaceLicenseRefreshEndpoint(BaseAPIView):
|
||||
plan=response.get("plan"),
|
||||
last_synced_at=timezone.now(),
|
||||
trial_end_date=response.get("trial_end_date"),
|
||||
has_activated_free_trial=response.get(
|
||||
"has_activated_free_trial", False
|
||||
),
|
||||
has_added_payment_method=response.get(
|
||||
"has_added_payment_method", False
|
||||
),
|
||||
)
|
||||
|
||||
# Return the response
|
||||
@@ -401,6 +434,12 @@ class WorkspaceLicenseSyncEndpoint(BaseAPIView):
|
||||
workspace_license.trial_end_date = request.data.get(
|
||||
"trial_end_date"
|
||||
)
|
||||
workspace_license.has_activated_free_trial = request.data.get(
|
||||
"has_activated_free_trial", False
|
||||
)
|
||||
workspace_license.has_added_payment_method = request.data.get(
|
||||
"has_added_payment_method", False
|
||||
)
|
||||
workspace_license.save()
|
||||
# If the workspace license is not present, then fetch the license from the payment server and create it
|
||||
else:
|
||||
@@ -417,6 +456,12 @@ class WorkspaceLicenseSyncEndpoint(BaseAPIView):
|
||||
plan=request.data.get("plan"),
|
||||
last_synced_at=timezone.now(),
|
||||
trial_end_date=request.data.get("trial_end_date"),
|
||||
has_activated_free_trial=request.data.get(
|
||||
"has_activated_free_trial", False
|
||||
),
|
||||
has_added_payment_method=request.data.get(
|
||||
"has_added_payment_method", False
|
||||
),
|
||||
)
|
||||
|
||||
# Return the response
|
||||
|
||||
@@ -143,6 +143,16 @@ class UpgradeSubscriptionEndpoint(BaseAPIView):
|
||||
workspace_license.trial_end_date = (
|
||||
workspace_license_response.get("trial_end_date")
|
||||
)
|
||||
workspace_license.has_activated_free_trial = (
|
||||
workspace_license_response.get(
|
||||
"has_activated_free_trial"
|
||||
)
|
||||
)
|
||||
workspace_license.has_added_payment_method = (
|
||||
workspace_license_response.get(
|
||||
"has_added_payment_method"
|
||||
)
|
||||
)
|
||||
workspace_license.save()
|
||||
else:
|
||||
# Create a new workspace license
|
||||
@@ -168,6 +178,12 @@ class UpgradeSubscriptionEndpoint(BaseAPIView):
|
||||
"trial_end_date"
|
||||
),
|
||||
plan=workspace_license_response.get("plan"),
|
||||
has_activated_free_trial=workspace_license_response.get(
|
||||
"has_activated_free_trial"
|
||||
),
|
||||
has_added_payment_method=workspace_license_response.get(
|
||||
"has_added_payment_method"
|
||||
),
|
||||
)
|
||||
|
||||
# Return the response
|
||||
|
||||
4
packages/types/src/payment.d.ts
vendored
4
packages/types/src/payment.d.ts
vendored
@@ -23,4 +23,8 @@ export type IWorkspaceProductSubscription = {
|
||||
interval?: "month" | "year" | null;
|
||||
current_period_end_date: string | null;
|
||||
is_offline_payment: boolean;
|
||||
trial_end_date: string | undefined;
|
||||
purchased_seats: number | undefined;
|
||||
has_activated_free_trial: boolean;
|
||||
has_added_payment_method: boolean;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
||||
<AuthenticationWrapper>
|
||||
<CommandPalette />
|
||||
<WorkspaceAuthWrapper>
|
||||
<div className="relative flex h-screen w-full overflow-hidden">
|
||||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
<AppSidebar />
|
||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
||||
{children}
|
||||
|
||||
@@ -9,6 +9,8 @@ import "@/styles/react-day-picker.css";
|
||||
import { SITE_NAME, SITE_DESCRIPTION } from "@/constants/meta";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// plane web components
|
||||
import { FreeTrialBanner } from "@/plane-web/components/license/free-trial-banner";
|
||||
// local
|
||||
import { AppProvider } from "./provider";
|
||||
|
||||
@@ -70,7 +72,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<body>
|
||||
<div id="context-menu-portal" />
|
||||
<AppProvider>
|
||||
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>{children}</div>
|
||||
<div className={`h-screen w-full overflow-hidden bg-custom-background-100 relative flex flex-col`}>
|
||||
<div className="flex-shrink-0">
|
||||
{/* free trial banner */}
|
||||
<FreeTrialBanner />
|
||||
</div>
|
||||
<div className="w-full h-full overflow-hidden">{children}</div>
|
||||
</div>
|
||||
</AppProvider>
|
||||
</body>
|
||||
{process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && (
|
||||
|
||||
@@ -67,6 +67,62 @@ export const CloudEditionBadge = observer(() => {
|
||||
</Loader>
|
||||
);
|
||||
|
||||
// derived values
|
||||
const isInTrialPeriod =
|
||||
subscriptionDetail?.has_activated_free_trial && subscriptionDetail?.trial_end_date ? true : false;
|
||||
const isTrialCompleted =
|
||||
subscriptionDetail?.has_activated_free_trial && !subscriptionDetail?.trial_end_date ? true : false;
|
||||
|
||||
const renderButtonText = () => {
|
||||
switch (subscriptionDetail.product) {
|
||||
case "FREE": {
|
||||
if (subscriptionDetail?.has_activated_free_trial) {
|
||||
return "Buy Pro";
|
||||
} else {
|
||||
return "Try 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 "Try Pro";
|
||||
}
|
||||
}
|
||||
default:
|
||||
return "Try Pro";
|
||||
}
|
||||
};
|
||||
|
||||
const showPaymentButton = () => {
|
||||
switch (subscriptionDetail.product) {
|
||||
case "FREE": {
|
||||
if (subscriptionDetail?.has_activated_free_trial) {
|
||||
return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case "PRO":
|
||||
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;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const showPlaneProButton = showPaymentButton() ? false : subscriptionDetail.product === "PRO" ? true : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaidPlanSuccessModal
|
||||
@@ -74,37 +130,36 @@ export const CloudEditionBadge = observer(() => {
|
||||
isOpen={isProPlanSuccessModalOpen}
|
||||
handleClose={() => setProPlanSuccessModalOpen(false)}
|
||||
/>
|
||||
{subscriptionDetail.product === "FREE" && (
|
||||
<>
|
||||
{/* This modal is intentionally placed inside the condition to avoid unnecessary calls to list product endpoint. */}
|
||||
<ProPlanCloudUpgradeModal
|
||||
isOpen={isProPlanModalOpen}
|
||||
handleClose={() => toggleProPlanModal(false)}
|
||||
yearlyPlan={false}
|
||||
handleSuccessModal={() => setProPlanSuccessModalOpen(true)}
|
||||
/>
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
className="w-full cursor-pointer rounded-2xl px-4 py-1.5 text-center text-sm font-medium outline-none"
|
||||
onClick={handleProPlanPurchaseModalOpen}
|
||||
>
|
||||
{subscriptionDetail.product === "FREE" ? "Upgrade to Pro" : "Get Pro yearly"}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
<ProPlanCloudUpgradeModal
|
||||
isOpen={isProPlanModalOpen}
|
||||
handleClose={() => toggleProPlanModal(false)}
|
||||
yearlyPlan={false}
|
||||
handleSuccessModal={() => setProPlanSuccessModalOpen(true)}
|
||||
canFetchProducts={subscriptionDetail.product === "FREE" || isInTrialPeriod || isTrialCompleted}
|
||||
/>
|
||||
|
||||
{showPaymentButton() && (
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
className="w-full cursor-pointer rounded-2xl px-4 py-1.5 text-center text-sm font-medium outline-none"
|
||||
onClick={handleProPlanPurchaseModalOpen}
|
||||
>
|
||||
{renderButtonText()}
|
||||
</Button>
|
||||
)}
|
||||
{subscriptionDetail.product === "PRO" && (
|
||||
<>
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
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={14} height={14} />
|
||||
{"Plane Pro"}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
{showPlaneProButton && (
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="accent-primary"
|
||||
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={14} height={14} />
|
||||
Plane Pro
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
69
web/ee/components/license/free-trial-banner.tsx
Normal file
69
web/ee/components/license/free-trial-banner.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
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
|
||||
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) 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}
|
||||
/>
|
||||
|
||||
<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
|
||||
<a
|
||||
href="https://plane.so/pro"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-4 hover:font-bold transition-all"
|
||||
>
|
||||
Pro features.
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Button variant="outline-primary" size="sm" onClick={() => setPricingModalOpen(true)}>
|
||||
Upgrade to Pro
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -24,6 +24,7 @@ export type ProPlanCloudUpgradeModalProps = {
|
||||
handleClose: () => void;
|
||||
yearlyPlan?: boolean;
|
||||
handleSuccessModal?: () => void;
|
||||
canFetchProducts?: boolean;
|
||||
};
|
||||
|
||||
// constants
|
||||
@@ -36,7 +37,7 @@ export const calculateYearlyDiscount = (monthlyPrice: number, yearlyPricePerMont
|
||||
};
|
||||
|
||||
export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (props) => {
|
||||
const { isOpen, handleClose, yearlyPlan, handleSuccessModal } = props;
|
||||
const { isOpen, handleClose, yearlyPlan, handleSuccessModal, canFetchProducts = true } = props;
|
||||
// params
|
||||
const { workspaceSlug } = useParams();
|
||||
// states
|
||||
@@ -47,13 +48,17 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
const {
|
||||
membership: { currentWorkspaceRole },
|
||||
} = useUser();
|
||||
const { fetchWorkspaceSubscribedPlan, freeTrialSubscription } = useWorkspaceSubscription();
|
||||
const {
|
||||
currentWorkspaceSubscribedPlanDetail: subscriptionDetail,
|
||||
fetchWorkspaceSubscribedPlan,
|
||||
freeTrialSubscription,
|
||||
} = useWorkspaceSubscription();
|
||||
// derived values
|
||||
const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN;
|
||||
// fetch products
|
||||
const { isLoading: isProductsAPILoading, data } = useSWR(
|
||||
workspaceSlug ? "CLOUD_PAYMENT_PRODUCTS" : null,
|
||||
workspaceSlug ? () => paymentService.listProducts(workspaceSlug.toString()) : null,
|
||||
workspaceSlug && canFetchProducts ? "CLOUD_PAYMENT_PRODUCTS" : null,
|
||||
workspaceSlug && canFetchProducts ? () => paymentService.listProducts(workspaceSlug.toString()) : null,
|
||||
{
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
@@ -115,6 +120,7 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
});
|
||||
};
|
||||
|
||||
// handling the payment link when the free trial is disabled
|
||||
const handlePaymentLink = (priceId: string) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
@@ -145,6 +151,40 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
}
|
||||
};
|
||||
|
||||
// handling the payment link when the free trial is enabled
|
||||
const handleSubscriptionPageRedirection = () => {
|
||||
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
|
||||
.getWorkspaceSubscriptionPageLink(workspaceSlug.toString())
|
||||
.then((response) => {
|
||||
if (response.url) {
|
||||
window.open(response.url, "_blank");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to redirect to subscription page. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
// handle trial
|
||||
const handleTrial = async (productId: string, priceId: string) => {
|
||||
try {
|
||||
setTrialLoader(true);
|
||||
@@ -165,18 +205,29 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
}
|
||||
};
|
||||
|
||||
// derived values
|
||||
const monthlyPrice =
|
||||
(orderBy(proProduct?.prices || [], ["recurring"], ["desc"])?.find((price) => price.recurring === "month")
|
||||
?.unit_amount || 0) / 100;
|
||||
const yearlyPrice =
|
||||
(orderBy(proProduct?.prices || [], ["recurring"], ["desc"])?.find((price) => price.recurring === "year")
|
||||
?.unit_amount || 0) / 1000;
|
||||
// derived values
|
||||
const yearlyDiscount = calculateYearlyDiscount(monthlyPrice, yearlyPrice);
|
||||
const isInTrailPeriod =
|
||||
subscriptionDetail?.has_activated_free_trial && subscriptionDetail?.trial_end_date ? true : false;
|
||||
const isTrialCompleted =
|
||||
subscriptionDetail?.has_activated_free_trial && !subscriptionDetail?.trial_end_date ? 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 && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
<Dialog.Title as="h2" className="text-2xl font-bold leading-6 mt-6 flex justify-center items-center">
|
||||
Upgrade to Pro, yearly for flat {yearlyDiscount}% off.
|
||||
</Dialog.Title>
|
||||
@@ -207,17 +258,19 @@ export const ProPlanCloudUpgradeModal: FC<ProPlanCloudUpgradeModalProps> = (prop
|
||||
</Loader>
|
||||
</Loader>
|
||||
)}
|
||||
|
||||
{proProduct && (
|
||||
<ProPlanUpgrade
|
||||
proProduct={proProduct}
|
||||
basePlan="Free"
|
||||
features={PRO_PLAN_FEATURES_MAP}
|
||||
isLoading={isLoading}
|
||||
handlePaymentLink={handlePaymentLink}
|
||||
handlePaymentLink={isInTrailPeriod ? handleSubscriptionPageRedirection : handlePaymentLink}
|
||||
yearlyPlanOnly={yearlyPlan}
|
||||
trialLoader={trialLoader}
|
||||
handleTrial={handleTrial}
|
||||
yearlyDiscount={yearlyDiscount}
|
||||
showTrialButton={!isInTrailPeriod && !isTrialCompleted}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ export type ProPlanUpgradeProps = {
|
||||
trialLoader?: boolean;
|
||||
handleTrial?: (productId: string, priceId: string) => void;
|
||||
yearlyDiscount?: number;
|
||||
showTrialButton?: boolean;
|
||||
};
|
||||
|
||||
export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
@@ -34,6 +35,7 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
trialLoader = false,
|
||||
handleTrial,
|
||||
yearlyDiscount,
|
||||
showTrialButton = false,
|
||||
} = props;
|
||||
// derived values
|
||||
const yearlyPrice = orderBy(proProduct?.prices || [], ["recurring"], ["desc"]).find(
|
||||
@@ -108,15 +110,18 @@ export const ProPlanUpgrade: FC<ProPlanUpgradeProps> = (props) => {
|
||||
: "Redirecting to Stripe..."
|
||||
: "Upgrade to Pro"}
|
||||
</button>
|
||||
{/* free trail button */}
|
||||
<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"
|
||||
onClick={() => handleTrial && handleTrial(proProduct?.id, price.id)}
|
||||
>
|
||||
<span>Start free trial</span>
|
||||
<div className="w-3 h-3">{trialLoader && <Loader size={12} className="animate-spin" />}</div>
|
||||
</button>
|
||||
|
||||
{/* trail button */}
|
||||
{showTrialButton && (
|
||||
<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"
|
||||
onClick={() => handleTrial && handleTrial(proProduct?.id, price.id)}
|
||||
>
|
||||
<span>Start free trial</span>
|
||||
<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
|
||||
|
||||
@@ -57,6 +57,11 @@ export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore {
|
||||
is_canceled: response?.is_canceled || 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,
|
||||
});
|
||||
});
|
||||
return response;
|
||||
|
||||
Reference in New Issue
Block a user