diff --git a/apiserver/plane/payment/utils/workspace_license_request.py b/apiserver/plane/payment/utils/workspace_license_request.py index 1e50572767..d7c90d096c 100644 --- a/apiserver/plane/payment/utils/workspace_license_request.py +++ b/apiserver/plane/payment/utils/workspace_license_request.py @@ -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), } diff --git a/apiserver/plane/payment/views/payment.py b/apiserver/plane/payment/views/payment.py index c1784e10e9..f16d212cde 100644 --- a/apiserver/plane/payment/views/payment.py +++ b/apiserver/plane/payment/views/payment.py @@ -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"}, diff --git a/apiserver/plane/payment/views/product.py b/apiserver/plane/payment/views/product.py index 0b89a7c8f9..776923cd58 100644 --- a/apiserver/plane/payment/views/product.py +++ b/apiserver/plane/payment/views/product.py @@ -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: diff --git a/packages/types/src/payment.d.ts b/packages/types/src/payment.d.ts index 815a3a7aff..579c2e37db 100644 --- a/packages/types/src/payment.d.ts +++ b/packages/types/src/payment.d.ts @@ -14,12 +14,13 @@ export type IPaymentProduct = { id: string; name: string; type: Omit; + 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; }; diff --git a/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx b/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx index 2e93767461..966fc602e8 100644 --- a/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx +++ b/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx @@ -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(() => {
Choose your billing frequency
- {yearlyDiscountedPrice && ( + {!!yearlyDiscountedPrice && (
{`Upgrade to our yearly plan and get ${Math.round(yearlyDiscountedPrice)}% off.`}
)}
- {isFetching || isLoadingProduct || workspaceSubscriptionLoading ? ( + {isLoadingProduct ? (
@@ -177,13 +127,13 @@ const CloudUpgradePlanPage = observer(() => { {plan.recurring === selectedPlan?.recurring && }
- {totalWorkspaceMembers && ( + {totalPaidMembers && (
${renderPlanPricing(plan.unit_amount, 1, plan.recurring)} - {` per user per ${plan.recurring} x ${totalWorkspaceMembers}`} + {` per user per ${plan.recurring} x ${totalPaidMembers}`}
)} @@ -193,9 +143,9 @@ const CloudUpgradePlanPage = observer(() => {
- {totalWorkspaceMembers && ( + {totalPaidMembers && ( - ${renderPlanPricing(plan.unit_amount, totalWorkspaceMembers, plan.recurring)} + ${renderPlanPricing(plan.unit_amount, totalPaidMembers, plan.recurring)} )} @@ -209,15 +159,10 @@ const CloudUpgradePlanPage = observer(() => { {selectedPlan?.recurring && selectedPlan.unit_amount ? ( <> - ) : ( diff --git a/web/app/upgrade/pro/cloud/page.tsx b/web/app/upgrade/pro/cloud/page.tsx index c90c4db271..98d56ca101 100644 --- a/web/app/upgrade/pro/cloud/page.tsx +++ b/web/app/upgrade/pro/cloud/page.tsx @@ -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={
-
+
{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(() => {
), value: workspace.slug, - disabled: ["PRO"].includes(workspaceSubscription(workspace)), + disabled: workspace.is_billing_active, })) : [] } diff --git a/web/ee/components/license/cloud-badge.tsx b/web/ee/components/license/cloud-badge.tsx index ce93f5b619..9a4be308fe 100644 --- a/web/ee/components/license/cloud-badge.tsx +++ b/web/ee/components/license/cloud-badge.tsx @@ -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 ( <> toggleProPlanModal(false)} - yearlyPlan={false} handleSuccessModal={() => handleSuccessModalToggle(true)} /> - {showPaymentButton() && ( + {subscriptionDetail.show_payment_button && ( )} - {showPlaneProButton && ( + {!subscriptionDetail.show_payment_button && ( )} - - {/* Fallback text */} - {!showPaymentButton() && !showPlaneProButton && ( -
- Enterprise Edition -
- )} ); }); diff --git a/web/ee/components/license/free-trial-banner.tsx b/web/ee/components/license/free-trial-banner.tsx index b69dcc791d..3bff59216a 100644 --- a/web/ee/components/license/free-trial-banner.tsx +++ b/web/ee/components/license/free-trial-banner.tsx @@ -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. */} - setPricingModalOpen(false)} - yearlyPlan={false} - /> + setPricingModalOpen(false)} />
- 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  { 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 && ( <> -
- Free Trial +
+ Pro Trial
- (Free trial ends on:{" "} + (Pro trial ends on:{" "} {renderFormattedDate(currentWorkspaceSubscribedPlanDetail?.trial_end_date)})
diff --git a/web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx b/web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx index 468326d3c7..815616af22 100644 --- a/web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx +++ b/web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx @@ -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 = (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 = (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 = (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 = (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 = (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 = (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 (
- {isInTrailPeriod && ( + {subscriptionDetail?.is_on_trial && (
-
- Free Trial +
+ Pro trial in progress
)} - + Upgrade to Pro, yearly for flat {yearlyDiscount}% off. -
+

- 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.

@@ -273,16 +195,11 @@ export const ProPlanCloudUpgradeModal: FC = (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} /> )}

diff --git a/web/ee/components/license/pro-plan-self-host-upgrade-modal.tsx b/web/ee/components/license/pro-plan-self-host-upgrade-modal.tsx index f3482369b4..8edde6f9f6 100644 --- a/web/ee/components/license/pro-plan-self-host-upgrade-modal.tsx +++ b/web/ee/components/license/pro-plan-self-host-upgrade-modal.tsx @@ -37,6 +37,7 @@ export const ProPlanSelfHostUpgradeModal: FC = name: "Pro", type: "PRO", description: "Pro plan details for self-hosted users", + payment_quantity: 1, prices: [ { id: "pro_monthly", diff --git a/web/ee/components/license/pro-plan-upgrade.tsx b/web/ee/components/license/pro-plan-upgrade.tsx index e3e9d4dedc..15493f747a 100644 --- a/web/ee/components/license/pro-plan-upgrade.tsx +++ b/web/ee/components/license/pro-plan-upgrade.tsx @@ -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 = (props) => { features, handlePaymentLink, isLoading = false, - yearlyPlanOnly = false, verticalFeatureList = false, extraFeatures, trialLoader = false, @@ -38,14 +36,7 @@ export const ProPlanUpgrade: FC = (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 = (props) => {
- {!yearlyPlanOnly && ( - - {proProductPrices.map((price: IPaymentProductPrice) => ( - - 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 && ( - - -{yearlyDiscount}% - - )} - - - ))} - - )} + + {proProductPrices.map((price: IPaymentProductPrice) => ( + + 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 && ( + + -{yearlyDiscount}% + + )} + + + ))} +
{proProductPrices?.map((price: IPaymentProductPrice) => ( @@ -104,11 +93,7 @@ export const ProPlanUpgrade: FC = (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"} {/* trail button */} @@ -122,12 +107,6 @@ export const ProPlanUpgrade: FC = (props) => {
{trialLoader && }
)} - {yearlyPlanOnly && ( -
- We will charge your card on file at $5 per user per month for the total number of users in - your workspace and update your subscription from Pro, monthly or Pro, yearly. -
- )}
{`Everything in ${basePlan} +`}
diff --git a/web/ee/services/payment.service.ts b/web/ee/services/payment.service.ts index 7caf933fe9..1d72ee251d 100644 --- a/web/ee/services/payment.service.ts +++ b/web/ee/services/payment.service.ts @@ -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 { 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; - }); - } } diff --git a/web/ee/store/subscription/subscription.store.ts b/web/ee/store/subscription/subscription.store.ts index e85f35d175..d495d7eb9f 100644 --- a/web/ee/store/subscription/subscription.store.ts +++ b/web/ee/store/subscription/subscription.store.ts @@ -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; diff --git a/web/ee/types/workspace.d.ts b/web/ee/types/workspace.d.ts index 85f4803aa8..c88af677cf 100644 --- a/web/ee/types/workspace.d.ts +++ b/web/ee/types/workspace.d.ts @@ -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; };