diff --git a/packages/types/src/payment.d.ts b/packages/types/src/payment.d.ts index 3ca78236b4..462b5773cb 100644 --- a/packages/types/src/payment.d.ts +++ b/packages/types/src/payment.d.ts @@ -22,4 +22,5 @@ export type IWorkspaceProductSubscription = { is_canceled?: boolean; interval?: "month" | "year" | null; current_period_end_date: string | null; + is_offline_payment: boolean; }; diff --git a/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx b/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx new file mode 100644 index 0000000000..f17652dae4 --- /dev/null +++ b/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +import { Check } from "lucide-react"; +// types +import { IPaymentProduct, IPaymentProductPrice } from "@plane/types"; +// ui +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 CloudUpgradePlanPage = observer(() => { + // states + const [selectedPlan, setSelectedPlan] = useState(null); + const [isLoading, setLoading] = useState(false); + // router + const { selectedWorkspace } = useParams(); + + // fetch workspace members + const { data: workspaceMembers, isLoading: isFetching } = useSWR( + selectedWorkspace ? `WORKSPACES_WITH_PLAN_DETAILS` : null, + selectedWorkspace ? () => workspaceService.fetchWorkspaceMembers(selectedWorkspace.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } + ); + + // fetch products + const { data, isLoading: isLoadingProduct } = useSWR( + selectedWorkspace ? "CLOUD_PAYMENT_PRODUCTS" : null, + selectedWorkspace ? () => paymentService.listProducts(selectedWorkspace.toString()) : null, + { + errorRetryCount: 2, + revalidateIfStale: false, + revalidateOnFocus: false, + } + ); + + const totalWorkspaceMembers = (workspaceMembers || [])?.filter((member) => member.role >= 15)?.length; + + const proProduct = (data || [])?.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 calculateYearlyDiscount = (monthlyAmount: number | undefined, yearlyAmount: number | undefined) => { + if (!monthlyAmount || !yearlyAmount) return undefined; + const yearlyCostIfPaidMonthly = monthlyAmount * 12; + const discountAmount = yearlyCostIfPaidMonthly - yearlyAmount; + const discountPercentage = (discountAmount / yearlyCostIfPaidMonthly) * 100; + + return discountPercentage; + }; + + const discountedPrice = calculateYearlyDiscount(monthlyPlan?.unit_amount, yearlyPlan?.unit_amount); + + const handleStripeCheckout = (priceId: string) => { + if (!selectedWorkspace) { + setToast({ + type: TOAST_TYPE.INFO, + title: "Please select a workspace to continue", + }); + return; + } + setLoading(true); + paymentService + .getCurrentWorkspacePaymentLink(selectedWorkspace.toString(), { + price_id: priceId, + 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); + }); + }; + + return ( +
+
+
Choose your billing frequency
+ {discountedPrice && ( +
{`Upgrade to our yearly plan and get ${Math.round(discountedPrice)}% off.`}
+ )} +
+ + {isFetching || isLoadingProduct ? ( +
+ + +
+ ) : ( +
+ {proProduct && + proProduct.prices.map((plan) => ( +
setSelectedPlan(plan)} + > +
+ + {plan.recurring === selectedPlan?.recurring && } + +
+ {totalWorkspaceMembers && ( +
+ {`$${plan.unit_amount / 100}`} + + {` per user per ${plan.recurring} x ${totalWorkspaceMembers}`} + +
+ )} + + {`Billed ${plan.recurring === "month" ? "monthly" : "yearly"}`} + +
+
+ +
+ {totalWorkspaceMembers && ( + {`$${(plan.unit_amount / 100) * totalWorkspaceMembers}`} + )} + + {plan.recurring === "month" ? "per month" : "per year"} + +
+
+ ))} +
+ )} + {selectedPlan?.recurring && selectedPlan.unit_amount ? ( + <> + + + ) : ( + <> + + + )} +
+ ); +}); + +export default CloudUpgradePlanPage; diff --git a/web/app/upgrade/pro/cloud/page.tsx b/web/app/upgrade/pro/cloud/page.tsx index ecfbe03461..e569b76c0f 100644 --- a/web/app/upgrade/pro/cloud/page.tsx +++ b/web/app/upgrade/pro/cloud/page.tsx @@ -50,34 +50,8 @@ const CloudUpgradePage = observer(() => { const isAnyWorkspaceAvailable = workspacesList && workspacesList?.length > 0; - const handlePaymentPageRedirection = () => { - if (!selectedWorkspace) { - setToast({ - type: TOAST_TYPE.INFO, - title: "Please select a workspace to continue", - }); - return; - } - setIsLoading(true); - paymentService - .getPaymentLink({ - slug: selectedWorkspace, - }) - .then((response) => { - if (response.payment_link) { - window.open(response.payment_link, "_blank"); - } - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Failed to generate payment link. Please try again.", - }); - }) - .finally(() => { - setIsLoading(false); - }); + const handleNextStep = () => { + router.push(`/upgrade/pro/cloud/${selectedWorkspace}`); }; const handleSignOut = async () => { @@ -104,60 +78,59 @@ const CloudUpgradePage = observer(() => { label={
- {isAnyWorkspaceAvailable ? "Choose your workspace" : "No eligible workspace found!"} + {isAnyWorkspaceAvailable + ? "Choose your workspace" + : "We didn't find an eligible workspace for this upgrade. Try another email address."}
{isAnyWorkspaceAvailable ? `We found the following workspaces eligible for Pro. If you want to upgrade a different workspace, log in - with that email` - : `We couldn't find any Pro eligible workspace. Try a different email address and make sure you are an admin of the workspace you are trying to upgrade.`} + with that email and make sure you are an admin of the workspace you want to upgrade.` + : `We couldn't find any Pro eligible workspace. Try a different email address and make sure you are an admin of the workspace you want to upgrade.`}
} options={ isAnyWorkspaceAvailable ? workspacesList.map((workspace) => ({ - label: ( -
-
-
- {workspace?.logo && workspace.logo !== "" ? ( - {workspace.name} - ) : ( - - {workspace?.name[0]} - - )} -
-
-
-
{truncateText(workspace?.name, 40)}
-
- {workspace.product === "PRO" && ( + label: ( +
- -
- Pro -
-
+
+ {workspace?.logo && workspace.logo !== "" ? ( + {workspace.name} + ) : ( + + {workspace?.name[0]} + + )} +
- )} -
- ), - value: workspace.slug, - disabled: workspace.product !== "FREE", - })) +
+
{truncateText(workspace?.name, 40)}
+
+ {workspace.product === "PRO" && ( +
+ +
+ Pro +
+
+
+ )} +
+ ), + value: workspace.slug, + disabled: workspace.product !== "FREE", + })) : [] } className="w-full" @@ -172,11 +145,11 @@ const CloudUpgradePage = observer(() => { {isAnyWorkspaceAvailable ? ( ) : (
diff --git a/web/ee/components/license/plane-cloud-billing.tsx b/web/ee/components/license/plane-cloud-billing.tsx index cbe226e267..b1ea1c6740 100644 --- a/web/ee/components/license/plane-cloud-billing.tsx +++ b/web/ee/components/license/plane-cloud-billing.tsx @@ -107,21 +107,23 @@ export const PlaneCloudBilling: React.FC = observer(() => { (Expires on: {renderFormattedDate(endDate)})
) : ( -
- (Renew on: {renderFormattedDate(endDate)}) -
+
+ (Renew on: {renderFormattedDate(endDate)}) +
)} -
- -
+ {!currentWorkspaceSubscribedPlanDetail.is_offline_payment && ( +
+ +
+ )} )}