mirror of
https://github.com/makeplane/plane.git
synced 2025-12-28 16:06:33 +01:00
dev: pro plan upgrade screens. (#563)
* dev: pro plan upgrade screens. * dev: website upgrade workflow * chore: plan upgrade API integration. --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
committed by
sriram veeraghanta
parent
b25c4d07cf
commit
d98a3a06e6
@@ -4,6 +4,8 @@ from .views import (
|
||||
ProductEndpoint,
|
||||
PaymentLinkEndpoint,
|
||||
WorkspaceProductEndpoint,
|
||||
WebsitePaymentLinkEndpoint,
|
||||
WebsiteUserWorkspaceEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
@@ -22,4 +24,14 @@ urlpatterns = [
|
||||
PaymentLinkEndpoint.as_view(),
|
||||
name="products",
|
||||
),
|
||||
path(
|
||||
"website/payment-link/",
|
||||
WebsitePaymentLinkEndpoint.as_view(),
|
||||
name="website-payment-link",
|
||||
),
|
||||
path(
|
||||
"website/workspaces/",
|
||||
WebsiteUserWorkspaceEndpoint.as_view(),
|
||||
name="website-workspaces",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
from .product import ProductEndpoint, WorkspaceProductEndpoint
|
||||
from .payment import PaymentLinkEndpoint
|
||||
from .product import (
|
||||
ProductEndpoint,
|
||||
WorkspaceProductEndpoint,
|
||||
WebsiteUserWorkspaceEndpoint,
|
||||
)
|
||||
from .payment import PaymentLinkEndpoint, WebsitePaymentLinkEndpoint
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework.response import Response
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.app.permissions.workspace import WorkSpaceAdminPermission
|
||||
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
|
||||
@@ -19,7 +19,7 @@ from plane.utils.exception_logger import log_exception
|
||||
|
||||
class PaymentLinkEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
WorkspaceOwnerPermission,
|
||||
]
|
||||
|
||||
def post(self, request, slug):
|
||||
@@ -78,3 +78,83 @@ class PaymentLinkEndpoint(BaseAPIView):
|
||||
{"error": "error fetching payment link"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WebsitePaymentLinkEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Get the workspace slug
|
||||
slug = request.data.get("slug", False)
|
||||
# Check if slug is present
|
||||
if not slug:
|
||||
return Response(
|
||||
{"error": "slug is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# The user should be workspace admin
|
||||
if not WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member=request.user,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "You are not a admin of workspace"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# Get the workspace members
|
||||
workspace_members = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True, member__is_bot=False
|
||||
)
|
||||
.annotate(
|
||||
user_email=F("member__email"), user_id=F("member__id")
|
||||
)
|
||||
.values("user_email", "user_id")
|
||||
)
|
||||
|
||||
# Convert the user_id to string
|
||||
for member in workspace_members:
|
||||
member["user_id"] = str(member["user_id"])
|
||||
|
||||
# Check if the payment server base url is present
|
||||
if settings.PAYMENT_SERVER_BASE_URL:
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/website/payment-link/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={
|
||||
"workspace_id": str(workspace.id),
|
||||
"slug": slug,
|
||||
"customer_email": request.user.email,
|
||||
"members_list": list(workspace_members),
|
||||
"host": base_host(request=request, is_app=True),
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "error fetching payment link"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
log_exception(e)
|
||||
if e.response.status_code == 400:
|
||||
return Response(
|
||||
e.response.json(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{"error": "error fetching payment link"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@ import requests
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.db.models import CharField
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -11,7 +13,6 @@ from rest_framework.response import Response
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.app.permissions.workspace import (
|
||||
WorkSpaceAdminPermission,
|
||||
WorkspaceUserPermission,
|
||||
)
|
||||
from plane.db.models import WorkspaceMember, Workspace
|
||||
@@ -20,7 +21,7 @@ from plane.utils.exception_logger import log_exception
|
||||
|
||||
class ProductEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
WorkspaceUserPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
@@ -82,3 +83,46 @@ class WorkspaceProductEndpoint(BaseAPIView):
|
||||
{"error": "error fetching product details"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class WebsiteUserWorkspaceEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
# Get all the workspaces where the user is admin
|
||||
workspace_query = (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
role=20,
|
||||
)
|
||||
.annotate(uuid_str=Cast("workspace_id", CharField()))
|
||||
.values("uuid_str", "workspace__slug", "workspace__name", "workspace__logo")
|
||||
)
|
||||
|
||||
workspaces = [
|
||||
{
|
||||
"workspace_id": workspace["uuid_str"],
|
||||
"slug": workspace["workspace__slug"],
|
||||
"name": workspace["workspace__name"],
|
||||
"logo": workspace["workspace__logo"],
|
||||
}
|
||||
for workspace in workspace_query
|
||||
]
|
||||
|
||||
if settings.PAYMENT_SERVER_BASE_URL:
|
||||
response = requests.post(
|
||||
f"{settings.PAYMENT_SERVER_BASE_URL}/api/user-workspace-products/",
|
||||
headers={
|
||||
"content-type": "application/json",
|
||||
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
|
||||
},
|
||||
json={"workspaces": workspaces},
|
||||
)
|
||||
response.raise_for_status()
|
||||
response = response.json()
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "error fetching product details"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"SENTRY_MONITORING_ENABLED",
|
||||
"NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL",
|
||||
"NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL",
|
||||
"NEXT_PUBLIC_DISCO_BASE_URL"
|
||||
"NEXT_PUBLIC_DISCO_BASE_URL",
|
||||
"NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL"
|
||||
],
|
||||
"tasks": {
|
||||
"build": {
|
||||
|
||||
24
web/app/upgrade/pro/cloud/layout.tsx
Normal file
24
web/app/upgrade/pro/cloud/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function CloudUpgradeLayout(props: Props) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<PageHead title="Cloud upgrade - Plane" />
|
||||
<AuthenticationWrapper>
|
||||
{children}
|
||||
</AuthenticationWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
197
web/app/upgrade/pro/cloud/page.tsx
Normal file
197
web/app/upgrade/pro/cloud/page.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import useSWR from "swr";
|
||||
import { Button, TOAST_TYPE, Tooltip, getButtonStyling, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { RadioInput } from "@/components/estimates";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { truncateText } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import { PaymentService } from "@/plane-web/services/payment.service";
|
||||
|
||||
const workspaceService = new WorkspaceService();
|
||||
const paymentService = new PaymentService();
|
||||
|
||||
const CloudUpgradePage = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
// states
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState<string>("");
|
||||
// hooks
|
||||
const { data: currentUser, signOut } = useUser();
|
||||
// next themes
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { data: workspacesList, isLoading: isFetching } = useSWR(
|
||||
currentUser ? `WORKSPACES_WITH_PLAN_DETAILS` : null,
|
||||
currentUser ? () => workspaceService.getWorkspacesWithPlanDetails() : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<div className="size-full grid place-items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 handleSignOut = async () => {
|
||||
setIsLoading(true);
|
||||
await signOut()
|
||||
.then(() => {
|
||||
setTheme("system");
|
||||
router.push("/");
|
||||
})
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Failed to sign out. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-8">
|
||||
<RadioInput
|
||||
name="workspace-upgrade-radio-input"
|
||||
label={
|
||||
<div className="flex flex-col items-center gap-2 pb-4">
|
||||
<div className="text-3xl font-semibold">
|
||||
{isAnyWorkspaceAvailable ? "Choose your workspace" : "No eligible workspace found!"}
|
||||
</div>
|
||||
<div className="text-center text-base text-custom-text-300">
|
||||
{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.`}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
options={
|
||||
isAnyWorkspaceAvailable
|
||||
? workspacesList.map((workspace) => ({
|
||||
label: (
|
||||
<div className={`flex items-center gap-3 px-1`}>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="grid h-7 w-7 place-items-center rounded">
|
||||
{workspace?.logo && workspace.logo !== "" ? (
|
||||
<img
|
||||
src={workspace.logo}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded"
|
||||
alt={workspace.name}
|
||||
/>
|
||||
) : (
|
||||
<span className="grid h-7 w-7 justify-center place-items-center rounded bg-gray-700 px-3 py-1.5 uppercase text-sm text-white">
|
||||
{workspace?.name[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium">{truncateText(workspace?.name, 40)}</div>
|
||||
</div>
|
||||
{workspace.product === "PRO" && (
|
||||
<div className="flex-shrink-0">
|
||||
<Tooltip
|
||||
position="right"
|
||||
tooltipContent="You're already subscribed to pro plan for this workspace."
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"text-[#EA9924] bg-[#FFF7C2] rounded-md px-2 py-0 text-center text-xs font-medium outline-none"
|
||||
)}
|
||||
>
|
||||
Pro
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
value: workspace.slug,
|
||||
disabled: workspace.product !== "FREE",
|
||||
}))
|
||||
: []
|
||||
}
|
||||
className="w-full"
|
||||
wrapperClassName={cn({
|
||||
"w-full max-h-72 overflow-auto vertical-scrollbar scrollbar-sm flex flex-col gap-5 p-5 border border-custom-border-200 rounded-md":
|
||||
isAnyWorkspaceAvailable,
|
||||
})}
|
||||
buttonClassName="size-3.5 mt-0.5"
|
||||
selected={selectedWorkspace}
|
||||
onChange={(value) => setSelectedWorkspace(value)}
|
||||
/>
|
||||
{isAnyWorkspaceAvailable ? (
|
||||
<Button
|
||||
className="w-full px-2"
|
||||
onClick={handlePaymentPageRedirection}
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !selectedWorkspace}
|
||||
>
|
||||
{isLoading ? "Redirecting to Stripe..." : "Go to payment"}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="w-full flex gap-4 px-4">
|
||||
<Button className="w-full px-2" onClick={handleSignOut} loading={isLoading} disabled={isLoading}>
|
||||
Try another email address
|
||||
</Button>
|
||||
<Link href="/create-workspace" className={cn(getButtonStyling("outline-primary", "md"), "w-full px-2")}>
|
||||
Create a new workspace
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default CloudUpgradePage;
|
||||
45
web/app/upgrade/pro/layout.tsx
Normal file
45
web/app/upgrade/pro/layout.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
// helpers
|
||||
import { SwitchAccountDropdown } from "@/components/onboarding";
|
||||
import { EPageTypes } from "@/helpers/authentication.helper";
|
||||
// wrappers
|
||||
import { AuthenticationWrapper } from "@/lib/wrappers";
|
||||
// assets
|
||||
import PlaneProLogo from "@/public/plane-logos/plane-pro.svg";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function UpgradePlanLayout(props: Props) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full overflow-hidden">
|
||||
<AuthenticationWrapper pageType={EPageTypes.PUBLIC}>
|
||||
<PageHead title="Upgrade - Plane" />
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`/upgrade/pro`} className="h-[30px] w-full">
|
||||
<Image src={PlaneProLogo} alt="Plane pro logo" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
|
||||
<SwitchAccountDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center container h-[calc(100vh-240px)] mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 transition-all">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticationWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
web/app/upgrade/pro/page.tsx
Normal file
80
web/app/upgrade/pro/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { RadioInput } from "@/components/estimates";
|
||||
// hooks
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
|
||||
type TPlaneEditions = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
const PLANE_EDITIONS: TPlaneEditions = {
|
||||
cloud: {
|
||||
title: "Cloud account at app.plane.so",
|
||||
description: "You will log into your Plane account and select the workspace you want to upgrade",
|
||||
},
|
||||
"self-hosted": {
|
||||
title: "Self-hosted Plane",
|
||||
description: "Choose this if you self-host the Community Edition or One",
|
||||
},
|
||||
};
|
||||
|
||||
export default function UpgradePlanPage() {
|
||||
const router = useAppRouter();
|
||||
// states
|
||||
const [selectedEdition, setSelectedEdition] = useState<string>("cloud");
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (!selectedEdition) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.INFO,
|
||||
title: "Please select an edition to continue",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedEdition === "cloud") {
|
||||
router.push("/upgrade/pro/cloud");
|
||||
}
|
||||
if (selectedEdition === "self-hosted") {
|
||||
if (process.env.NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL) {
|
||||
window.open(process.env.NEXT_PUBLIC_PRO_SELF_HOSTED_PAYMENT_URL, "_blank");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center justify-center gap-8">
|
||||
<RadioInput
|
||||
name="edition-radio-input"
|
||||
label="Choose your edition"
|
||||
options={Object.keys(PLANE_EDITIONS).map((edition) => ({
|
||||
label: (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-base font-medium">{PLANE_EDITIONS[edition].title}</div>
|
||||
<div className="text-sm text-onboarding-text-300">{PLANE_EDITIONS[edition].description}</div>
|
||||
</div>
|
||||
),
|
||||
value: edition,
|
||||
}))}
|
||||
className="w-full"
|
||||
labelClassName="text-center text-3xl font-semibold pb-6"
|
||||
wrapperClassName="w-full flex flex-col gap-4"
|
||||
fieldClassName="border border-custom-border-200 rounded-md py-2 px-4 items-start"
|
||||
buttonClassName="size-3.5 mt-1"
|
||||
selected={selectedEdition}
|
||||
onChange={(value) => setSelectedEdition(value)}
|
||||
/>
|
||||
<Button className="w-full px-2" onClick={handleNextStep} disabled={!selectedEdition}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const RadioInput = ({
|
||||
id={`${name}_${index}`}
|
||||
name={name}
|
||||
className={cn(
|
||||
`group flex size-5 items-center justify-center rounded-full border border-custom-border-400 bg-custom-background-500 cursor-pointer`,
|
||||
`group flex flex-shrink-0 size-5 items-center justify-center rounded-full border border-custom-border-400 bg-custom-background-500 cursor-pointer`,
|
||||
selected === value ? `bg-custom-primary-200 border-custom-primary-100 ` : ``,
|
||||
disabled ? `bg-custom-background-200 border-custom-border-200 cursor-not-allowed` : ``,
|
||||
inputButtonClassName
|
||||
|
||||
@@ -30,6 +30,8 @@ export const SwitchAccountDropdown: FC<TSwitchAccountDropdownProps> = observer((
|
||||
? fullName
|
||||
: user?.email;
|
||||
|
||||
if (!displayName && !fullName) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-full shrink-0 justify-end">
|
||||
<SwitchAccountModal isOpen={showSwitchAccountModal} onClose={() => setShowSwitchAccountModal(false)} />
|
||||
|
||||
@@ -71,12 +71,11 @@ export const CloudProductsModal: FC<CloudProductsModalProps> = (props) => {
|
||||
setLoading(true);
|
||||
captureEvent("pro_plan_payment_link_clicked", { workspaceSlug });
|
||||
paymentService
|
||||
.getPaymentLink(workspaceSlug.toString(), {
|
||||
.getCurrentWorkspacePaymentLink(workspaceSlug.toString(), {
|
||||
price_id: priceId,
|
||||
product_id: proProduct?.id,
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("response", response);
|
||||
if (response.payment_link) {
|
||||
window.open(response.payment_link, "_blank");
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ export const PlaneOneModal: FC<PlaneOneModalProps> = observer((props) => {
|
||||
const instanceService = new InstanceService();
|
||||
|
||||
const { data } = useSWR("INSTANCE_CHANGELOG", () => instanceService.getInstanceChangeLog());
|
||||
console.log("data", data);
|
||||
|
||||
return (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
|
||||
@@ -17,7 +17,7 @@ export class PaymentService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
getPaymentLink(workspaceSlug: string, data = {}) {
|
||||
getCurrentWorkspacePaymentLink(workspaceSlug: string, data = {}) {
|
||||
return this.post(`/api/payments/workspaces/${workspaceSlug}/payment-link/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
@@ -32,4 +32,12 @@ export class PaymentService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getPaymentLink(data = {}) {
|
||||
return this.post(`/api/payments/website/payment-link/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// constants
|
||||
import { EViewAccess } from "@/constants/views";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// types
|
||||
import { TWorkspaceWithProductDetails } from "@/plane-web/types";
|
||||
// services
|
||||
import { WorkspaceService as CoreWorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
export class WorkspaceService extends CoreWorkspaceService {
|
||||
@@ -26,4 +31,12 @@ export class WorkspaceService extends CoreWorkspaceService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspacesWithPlanDetails(): Promise<TWorkspaceWithProductDetails[]> {
|
||||
return this.get(`/api/payments/website/workspaces/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./app";
|
||||
export * from "./pages";
|
||||
export * from "./workspace";
|
||||
|
||||
11
web/ee/types/workspace.d.ts
vendored
Normal file
11
web/ee/types/workspace.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// types
|
||||
import { TProductSubscriptionType } from "@plane/types";
|
||||
|
||||
export type TWorkspaceWithProductDetails = {
|
||||
workspace_id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
product: TProductSubscriptionType;
|
||||
current_period_end_date: string;
|
||||
};
|
||||
8
web/public/plane-logos/plane-pro.svg
Normal file
8
web/public/plane-logos/plane-pro.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="67" height="28" viewBox="0 0 67 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.127 1.92969C32.6856 1.92969 34.04 2.18945 35.1904 2.70897C36.3407 3.22848 37.2128 3.97993 37.8065 4.96331C38.4188 5.92813 38.725 7.06921 38.725 8.38656C38.725 9.70391 38.4188 10.8543 37.8065 11.8377C37.2128 12.8025 36.3407 13.5539 35.1904 14.092C34.04 14.6115 32.6856 14.8713 31.127 14.8713H27.4533V21.6899H23.2229V1.92969H31.127ZM30.8765 11.3089C32.0083 11.3089 32.8711 11.0584 33.4648 10.5574C34.0771 10.0564 34.3833 9.33283 34.3833 8.38656C34.3833 7.4403 34.0771 6.72596 33.4648 6.24355C32.8711 5.74258 32.0083 5.4921 30.8765 5.4921H27.4533V11.3089H30.8765Z" fill="#EA9924"/>
|
||||
<path d="M45.5163 6.93933L45.6276 11.281L45.3214 11.1419C45.5441 9.73174 45.9616 8.68343 46.5738 7.99692C47.1861 7.29186 48.0396 6.93933 49.1343 6.93933H50.4981V10.0564H49.1065C48.3087 10.0564 47.6593 10.1863 47.1583 10.4461C46.6573 10.6873 46.2863 11.0676 46.045 11.5872C45.8224 12.0881 45.7111 12.7283 45.7111 13.5075V21.6899H41.5364V6.93933H45.5163Z" fill="#EA9924"/>
|
||||
<path d="M59.2267 22.0239C57.7052 22.0239 56.3693 21.7085 55.219 21.0777C54.0871 20.4283 53.2058 19.5284 52.575 18.378C51.9627 17.2091 51.6565 15.8546 51.6565 14.3146C51.6565 12.7746 51.9627 11.4295 52.575 10.2791C53.2058 9.11018 54.0871 8.2103 55.219 7.57945C56.3693 6.93005 57.7052 6.60535 59.2267 6.60535C60.7296 6.60535 62.0469 6.93005 63.1787 7.57945C64.3291 8.2103 65.2197 9.11018 65.8505 10.2791C66.4814 11.4295 66.7968 12.7746 66.7968 14.3146C66.7968 15.8546 66.4814 17.2091 65.8505 18.378C65.2197 19.5284 64.3291 20.4283 63.1787 21.0777C62.0469 21.7085 60.7296 22.0239 59.2267 22.0239ZM59.2267 18.8233C60.2657 18.8233 61.0635 18.4337 61.6202 17.6544C62.1953 16.8566 62.4829 15.7433 62.4829 14.3146C62.4829 12.886 62.1953 11.782 61.6202 11.0027C61.0635 10.2049 60.2657 9.80596 59.2267 9.80596C58.1876 9.80596 57.3805 10.2049 56.8053 11.0027C56.2302 11.782 55.9426 12.886 55.9426 14.3146C55.9426 15.7433 56.2302 16.8566 56.8053 17.6544C57.3805 18.4337 58.1876 18.8233 59.2267 18.8233Z" fill="#EA9924"/>
|
||||
<path d="M19.223 1.92969H6.40771V8.68145H12.8154V15.1921H19.223V1.92969Z" fill="#EA9924"/>
|
||||
<path d="M6.40762 8.67969H0V15.1903H6.40762V8.67969Z" fill="#EA9924"/>
|
||||
<path d="M12.8153 15.1953H6.40771V21.7059H12.8153V15.1953Z" fill="#EA9924"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
Reference in New Issue
Block a user