diff --git a/apiserver/plane/payment/utils/workspace_license_request.py b/apiserver/plane/payment/utils/workspace_license_request.py index 59d6836746..046d3db39f 100644 --- a/apiserver/plane/payment/utils/workspace_license_request.py +++ b/apiserver/plane/payment/utils/workspace_license_request.py @@ -77,7 +77,8 @@ def has_upgraded(workspace_license): def is_on_trial(workspace_license): "Check if the workspace is on a trial" if ( - workspace_license.trial_end_date + workspace_license.subscription + and workspace_license.trial_end_date and workspace_license.trial_end_date >= timezone.now() ): return True @@ -87,8 +88,9 @@ def is_on_trial(workspace_license): def trial_remaining_days(workspace_license): """Calculate the remaining days of the trial""" if ( - workspace_license.trial_end_date - and workspace_license.trial_end_date > timezone.now() + workspace_license.subscription + 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 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/packages/types/src/payment.d.ts b/packages/types/src/payment.d.ts index 7341a8c62f..bbf6d1327c 100644 --- a/packages/types/src/payment.d.ts +++ b/packages/types/src/payment.d.ts @@ -19,7 +19,7 @@ export type IPaymentProduct = { 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; diff --git a/web/ee/components/license/cloud-badge.tsx b/web/ee/components/license/cloud-badge.tsx index 047c5186c7..9a4be308fe 100644 --- a/web/ee/components/license/cloud-badge.tsx +++ b/web/ee/components/license/cloud-badge.tsx @@ -64,7 +64,6 @@ export const CloudEditionBadge = observer(() => { toggleProPlanModal(false)} - yearlyPlan={false} handleSuccessModal={() => handleSuccessModalToggle(true)} /> diff --git a/web/ee/components/license/free-trial-banner.tsx b/web/ee/components/license/free-trial-banner.tsx index 4e569ef982..3bff59216a 100644 --- a/web/ee/components/license/free-trial-banner.tsx +++ b/web/ee/components/license/free-trial-banner.tsx @@ -26,11 +26,7 @@ export const FreeTrialBanner: FC = observer(() => { return ( <> {/* This modal is intentionally placed inside the condition to avoid unnecessary calls to list product endpoint. */} - setPricingModalOpen(false)} - yearlyPlan={false} - /> + setPricingModalOpen(false)} />
diff --git a/web/ee/components/license/plane-cloud-billing.tsx b/web/ee/components/license/plane-cloud-billing.tsx index 52a5144539..e5b0545ab8 100644 --- a/web/ee/components/license/plane-cloud-billing.tsx +++ b/web/ee/components/license/plane-cloud-billing.tsx @@ -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 && ( <> -
- 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 67e354923c..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 @@ -264,10 +195,7 @@ export const ProPlanCloudUpgradeModal: FC = (prop basePlan="Free" features={PRO_PLAN_FEATURES_MAP} isLoading={isLoading} - handlePaymentLink={(priceId: string) => - subscriptionDetail?.is_on_trial ? handleSubscriptionPageRedirection(priceId) : handlePaymentLink(priceId) - } - yearlyPlanOnly={yearlyPlan} + handlePaymentLink={handlePaymentLink} trialLoader={trialLoader} handleTrial={handleTrial} yearlyDiscount={yearlyDiscount} 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 c03fbab2e9..d495d7eb9f 100644 --- a/web/ee/store/subscription/subscription.store.ts +++ b/web/ee/store/subscription/subscription.store.ts @@ -63,7 +63,7 @@ export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore { runInAction(() => { set(this.subscribedPlan, workspaceSlug, { product: response?.product ?? "FREE", - is_canceled: response?.is_canceled ?? false, + 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, @@ -86,7 +86,7 @@ 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,