fix: sperating out plane one and cloud badges

This commit is contained in:
sriram veeraghanta
2024-06-19 20:26:14 +05:30
parent 896a89a83a
commit acfea71772
13 changed files with 167 additions and 233 deletions

View File

@@ -14,10 +14,10 @@ from .base import BaseAPIView
from plane.app.permissions.workspace import WorkSpaceAdminPermission
from plane.db.models import WorkspaceMember, Workspace
from plane.authentication.utils.host import base_host
from plane.utils.exception_logger import log_exception
class PaymentLinkEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
@@ -50,6 +50,10 @@ 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,
@@ -59,8 +63,8 @@ class PaymentLinkEndpoint(BaseAPIView):
"members_list": list(workspace_members),
"host": base_host(request=request, is_app=True),
},
headers={"content-type": "application/json"},
)
response.raise_for_status()
response = response.json()
return Response(response, status=status.HTTP_200_OK)
else:
@@ -68,7 +72,8 @@ class PaymentLinkEndpoint(BaseAPIView):
{"error": "error fetching payment link"},
status=status.HTTP_400_BAD_REQUEST,
)
except requests.exceptions.RequestException:
except requests.exceptions.RequestException as e:
log_exception(e)
return Response(
{"error": "error fetching payment link"},
status=status.HTTP_400_BAD_REQUEST,

View File

@@ -15,10 +15,10 @@ from plane.app.permissions.workspace import (
WorkspaceUserPermission,
)
from plane.db.models import WorkspaceMember, Workspace
from plane.utils.exception_logger import log_exception
class ProductEndpoint(BaseAPIView):
permission_classes = [
WorkSpaceAdminPermission,
]
@@ -31,8 +31,12 @@ class ProductEndpoint(BaseAPIView):
).count()
response = requests.get(
f"{settings.PAYMENT_SERVER_BASE_URL}/api/products/?quantity={count}",
headers={"content-type": "application/json"},
headers={
"content-type": "application/json",
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
},
)
response.raise_for_status()
response = response.json()
return Response(response, status=status.HTTP_200_OK)
else:
@@ -40,7 +44,8 @@ class ProductEndpoint(BaseAPIView):
{"error": "error fetching product details"},
status=status.HTTP_400_BAD_REQUEST,
)
except requests.exceptions.RequestException:
except requests.exceptions.RequestException as e:
log_exception(e)
return Response(
{"error": "error fetching product details"},
status=status.HTTP_400_BAD_REQUEST,
@@ -48,7 +53,6 @@ class ProductEndpoint(BaseAPIView):
class WorkspaceProductEndpoint(BaseAPIView):
permission_classes = [
WorkspaceUserPermission,
]
@@ -59,8 +63,12 @@ class WorkspaceProductEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug)
response = requests.get(
f"{settings.PAYMENT_SERVER_BASE_URL}/api/products/workspace-products/{str(workspace.id)}/",
headers={"content-type": "application/json"},
headers={
"content-type": "application/json",
"x-api-key": settings.PAYMENT_SERVER_AUTH_TOKEN,
},
)
response.raise_for_status()
response = response.json()
return Response(response, status=status.HTTP_200_OK)
else:
@@ -68,7 +76,8 @@ class WorkspaceProductEndpoint(BaseAPIView):
{"error": "error fetching product details"},
status=status.HTTP_400_BAD_REQUEST,
)
except requests.exceptions.RequestException:
except requests.exceptions.RequestException as e:
log_exception(e)
return Response(
{"error": "error fetching product details"},
status=status.HTTP_400_BAD_REQUEST,

View File

@@ -361,3 +361,4 @@ APP_BASE_URL = os.environ.get("APP_BASE_URL")
# Cloud server base url
PAYMENT_SERVER_BASE_URL = os.environ.get("PAYMENT_SERVER_BASE_URL", False)
PAYMENT_SERVER_AUTH_TOKEN = os.environ.get("PAYMENT_SERVER_AUTH_TOKEN", "")

View File

@@ -16,6 +16,6 @@ export type IPaymentProduct = {
};
export type IWorkspaceProductSubscription = {
product: FREE | PRO | ULTIMATE;
product: "FREE" | "PRO" | "ULTIMATE";
expiry_date: string | null;
};

View File

@@ -26,36 +26,25 @@
"SENTRY_MONITORING_ENABLED",
"NEXT_PUBLIC_PRO_PLAN_MONTHLY_REDIRECT_URL",
"NEXT_PUBLIC_PRO_PLAN_YEARLY_REDIRECT_URL",
"NEXT_PUBLIC_IS_MULTI_TENANT"
"NEXT_PUBLIC_DISCO_BASE_URL"
],
"tasks": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
".next/**",
"dist/**"
]
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"develop": {
"cache": false,
"persistent": true,
"dependsOn": [
"^build"
]
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true,
"dependsOn": [
"^build"
]
"dependsOn": ["^build"]
},
"test": {
"dependsOn": [
"^build"
],
"dependsOn": ["^build"],
"outputs": []
},
"lint": {

View File

@@ -1,112 +0,0 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { Tooltip, Button, getButtonStyling } from "@plane/ui";
// hooks
import { cn } from "@/helpers/common.helper";
import { useEventTracker, useInstance } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneOneModal, CloudProductsModal, ProPlanDetailsModal } from "@/plane-web/components/license";
// assets
import PlaneOneLogo from "@/public/plane-logos/plane-one.svg";
// services
import { DiscoService } from "@/services/disco.service";
import packageJson from "package.json";
const discoService = new DiscoService();
export const PlaneBadge: React.FC = observer(() => {
// params
const { workspaceSlug } = useParams();
// states
const [isProPlanModalOpen, setIsProPlanModalOpen] = useState(false);
const [isProPlanDetailsModalOpen, setProPlanDetailsModalOpen] = useState(false);
const [isPlaneOneModalOpen, setIsPlaneOneModalOpen] = useState(false);
// hooks
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const { instance } = useInstance();
// fetch workspace current plane information
const { data } = useSWR(
workspaceSlug ? "WORKSPACE_CURRENT_PLANE" : null,
workspaceSlug ? () => discoService.getWorkspaceCurrentPlane(workspaceSlug.toString()) : null
);
console.log("data", data);
const handleProPlanPurchaseModalOpen = () => {
setIsProPlanModalOpen(true);
captureEvent("pro_plan_modal_opened", {});
};
const handlePlaneOneModalOpen = () => {
setIsPlaneOneModalOpen(true);
captureEvent("plane_one_modal_opened", {});
};
// const handleProPlanDetailsModalOpen = () => {
// setProPlanDetailsModalOpen(true);
// };
if (process.env.NEXT_PUBLIC_IS_MULTI_TENANT === "1") {
return (
<>
<CloudProductsModal isOpen={isProPlanModalOpen} handleClose={() => setIsProPlanModalOpen(false)} />
<ProPlanDetailsModal isOpen={isProPlanDetailsModalOpen} handleClose={() => setProPlanDetailsModalOpen(false)} />
{data && data.product === "s" && (
<Button
variant="outline-primary"
className="w-1/2 cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
onClick={handleProPlanPurchaseModalOpen}
>
Upgrade to Pro
</Button>
)}
{data && data.product === "FREE" && (
<div className="w-1/2 flex justify-start">
<span className="items-center justify-center px-3.5 py-0.5 text-xs leading-4 rounded-xl bg-custom-primary-100/10 text-custom-primary-100">
Pro
</span>
</div>
)}
</>
);
}
if (instance?.product === "plane-one") {
return (
<>
<PlaneOneModal isOpen={isPlaneOneModalOpen} handleClose={() => setIsPlaneOneModalOpen(false)} />
<Tooltip tooltipContent={`Version: ${instance.current_version}`} isMobile={isMobile}>
<button
tabIndex={-1}
className={cn(
getButtonStyling("accent-primary", "md"),
"w-fit cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
)}
onClick={handlePlaneOneModalOpen}
>
<Image src={PlaneOneLogo} alt="Plane One" width={24} height={24} />
{"Plane One"}
</button>
</Tooltip>
</>
);
}
return (
<>
<Tooltip tooltipContent={`Version: v${packageJson.version}`} isMobile={isMobile}>
<div className="w-1/2 cursor-default rounded-md bg-green-500/10 px-2 py-1 text-center text-xs font-medium text-green-500 outline-none leading-6">
Enterprise Edition
</div>
</Tooltip>
</>
);
});

View File

@@ -0,0 +1,60 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { Button } from "@plane/ui";
// hooks
import { useEventTracker } from "@/hooks/store";
// enterprise imports
import { CloudProductsModal, ProPlanDetailsModal } from "@/plane-web/components/license";
import { useWorkspaceSubscription } from "@/plane-web/hooks/store";
export const CloudEditionBadge = observer(() => {
// params
const { workspaceSlug } = useParams();
// states
const [isProPlanModalOpen, setIsProPlanModalOpen] = useState(false);
const [isProPlanDetailsModalOpen, setProPlanDetailsModalOpen] = useState(false);
// hooks
const { captureEvent } = useEventTracker();
const { fetchWorkspaceSubscribedPlan, subscribedPlan } = useWorkspaceSubscription();
// fetch workspace current plane information
useSWR(
workspaceSlug && process.env.NEXT_PUBLIC_DISCO_BASE_URL ? `WORKSPACE_CURRENT_PLAN_${workspaceSlug}` : null,
workspaceSlug && process.env.NEXT_PUBLIC_DISCO_BASE_URL
? () => fetchWorkspaceSubscribedPlan(workspaceSlug.toString())
: null,
{
errorRetryCount: 2,
}
);
const handleProPlanPurchaseModalOpen = () => {
setIsProPlanModalOpen(true);
captureEvent("pro_plan_modal_opened", {});
};
return (
<>
<CloudProductsModal isOpen={isProPlanModalOpen} handleClose={() => setIsProPlanModalOpen(false)} />
<ProPlanDetailsModal isOpen={isProPlanDetailsModalOpen} handleClose={() => setProPlanDetailsModalOpen(false)} />
{subscribedPlan === "FREE" && (
<Button
variant="outline-primary"
className="w-full cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
onClick={handleProPlanPurchaseModalOpen}
>
Upgrade to Pro
</Button>
)}
{subscribedPlan === "PRO" && (
<div className="w-full flex justify-start">
<span className="items-center justify-center px-3.5 py-0.5 text-xs leading-4 rounded-xl bg-custom-primary-100/10 text-custom-primary-100">
Pro
</span>
</div>
)}
</>
);
});

View File

@@ -11,9 +11,9 @@ import { setToast, TOAST_TYPE } from "@plane/ui";
// store
import { useEventTracker } from "@/hooks/store";
// services
import { DiscoService } from "@/services/disco.service";
import { PaymentService } from "@/plane-web/services/payment.service";
const discoService = new DiscoService();
const paymentService = new PaymentService();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function classNames(...classes: any[]) {
@@ -51,12 +51,17 @@ export const CloudProductsModal: FC<CloudProductsModalProps> = (props) => {
const { captureEvent } = useEventTracker();
// fetch products
const { data } = useSWR(
workspaceSlug ? "CLOUD_PAYMENT_PRODUCTS" : null,
workspaceSlug ? () => discoService.listProducts(workspaceSlug.toString()) : null
workspaceSlug && process.env.NEXT_PUBLIC_DISCO_BASE_URL ? "CLOUD_PAYMENT_PRODUCTS" : null,
workspaceSlug && process.env.NEXT_PUBLIC_DISCO_BASE_URL
? () => paymentService.listProducts(workspaceSlug.toString())
: null,
{
errorRetryCount: 2,
}
);
const proProduct = data?.find((product: IPaymentProduct) => product?.type === "PRO");
const proProduct = (data || [])?.find((product: IPaymentProduct) => product?.type === "PRO");
const proProductPrices = orderBy(proProduct?.prices || [], ["recurring"], ["asc"]);
console.log("data", data);
console.log("proProductPrices", proProductPrices);
// states
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [tabIndex, setTabIndex] = useState(0);
@@ -66,7 +71,7 @@ export const CloudProductsModal: FC<CloudProductsModalProps> = (props) => {
if (!workspaceSlug) return;
setLoading(true);
captureEvent("pro_plan_payment_link_clicked", { workspaceSlug });
discoService
paymentService
.getPaymentLink(workspaceSlug.toString(), {
price_id: priceId,
product_id: proProduct?.id,

View File

@@ -3,3 +3,5 @@ export * from "./plane-one-modal";
export * from "./plane-one-billing";
export * from "./plane-cloud-billing";
export * from "./pro-plan-details-modal";
export * from "./plane-one-badge";
export * from "./cloud-badge";

View File

@@ -0,0 +1,48 @@
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
// ui
import { Tooltip, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance, useEventTracker } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneOneModal } from "@/plane-web/components/license";
// assets
import PlaneOneLogo from "@/public/plane-logos/plane-one.svg";
export const PlaneOneEditionBadge = observer(() => {
// states
const [isPlaneOneModalOpen, setIsPlaneOneModalOpen] = useState(false);
// hooks
const { isMobile } = usePlatformOS();
// store hooks
const { instance } = useInstance();
const { captureEvent } = useEventTracker();
const handlePlaneOneModalOpen = () => {
setIsPlaneOneModalOpen(true);
captureEvent("plane_one_modal_opened", {});
};
return (
<>
<PlaneOneModal isOpen={isPlaneOneModalOpen} handleClose={() => setIsPlaneOneModalOpen(false)} />
<Tooltip tooltipContent={`Version: ${instance?.current_version}`} isMobile={isMobile}>
<button
tabIndex={-1}
className={cn(
getButtonStyling("accent-primary", "md"),
"w-fit cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
)}
onClick={handlePlaneOneModalOpen}
>
<Image src={PlaneOneLogo} alt="Plane One" width={24} height={24} />
{"Plane One"}
</button>
</Tooltip>
</>
);
});

View File

@@ -1,100 +1,27 @@
import { useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import { useParams } from "next/navigation";
import useSWR from "swr";
// ui
import { Tooltip, Button, getButtonStyling } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
import { Tooltip } from "@plane/ui";
// hooks
import { useInstance, useEventTracker } from "@/hooks/store";
import { useInstance } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { PlaneOneModal, CloudProductsModal, ProPlanDetailsModal } from "@/plane-web/components/license";
import { useWorkspaceSubscription } from "@/plane-web/hooks/store";
// assets
import PlaneOneLogo from "@/public/plane-logos/plane-one.svg";
import { PlaneOneEditionBadge, CloudEditionBadge } from "@/plane-web/components/license";
// assets
import packageJson from "package.json";
export const WorkspaceEditionBadge = observer(() => {
// params
const { workspaceSlug } = useParams();
// states
const [isProPlanModalOpen, setIsProPlanModalOpen] = useState(false);
const [isProPlanDetailsModalOpen, setProPlanDetailsModalOpen] = useState(false);
const [isPlaneOneModalOpen, setIsPlaneOneModalOpen] = useState(false);
// hooks
const { captureEvent } = useEventTracker();
const { isMobile } = usePlatformOS();
const { instance } = useInstance();
const { fetchWorkspaceSubscribedPlan, subscribedPlan } = useWorkspaceSubscription();
// fetch workspace current plane information
useSWR(
workspaceSlug && process.env.NEXT_PUBLIC_IS_MULTI_TENANT === "1" ? `WORKSPACE_CURRENT_PLAN_${workspaceSlug}` : null,
workspaceSlug && process.env.NEXT_PUBLIC_IS_MULTI_TENANT === "1"
? () => fetchWorkspaceSubscribedPlan(workspaceSlug.toString())
: null,
{
errorRetryCount: 2,
}
);
const handleProPlanPurchaseModalOpen = () => {
setIsProPlanModalOpen(true);
captureEvent("pro_plan_modal_opened", {});
};
const DISCO_BASE_URL = process.env.NEXT_PUBLIC_DISCO_BASE_URL || "";
const handlePlaneOneModalOpen = () => {
setIsPlaneOneModalOpen(true);
captureEvent("plane_one_modal_opened", {});
};
if (process.env.NEXT_PUBLIC_IS_MULTI_TENANT === "1") {
return (
<>
<CloudProductsModal isOpen={isProPlanModalOpen} handleClose={() => setIsProPlanModalOpen(false)} />
<ProPlanDetailsModal isOpen={isProPlanDetailsModalOpen} handleClose={() => setProPlanDetailsModalOpen(false)} />
{subscribedPlan === "FREE" && (
<Button
variant="outline-primary"
className="w-full cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
onClick={handleProPlanPurchaseModalOpen}
>
Upgrade to Pro
</Button>
)}
{subscribedPlan === "PRO" && (
<div className="w-full flex justify-start">
<span className="items-center justify-center px-3.5 py-0.5 text-xs leading-4 rounded-xl bg-custom-primary-100/10 text-custom-primary-100">
Pro
</span>
</div>
)}
</>
);
if (DISCO_BASE_URL.length > 0) {
return <CloudEditionBadge />;
}
if (instance?.product === "plane-one") {
return (
<>
<PlaneOneModal isOpen={isPlaneOneModalOpen} handleClose={() => setIsPlaneOneModalOpen(false)} />
<Tooltip tooltipContent={`Version: ${instance.current_version}`} isMobile={isMobile}>
<button
tabIndex={-1}
className={cn(
getButtonStyling("accent-primary", "md"),
"w-fit cursor-pointer rounded-2xl px-3 py-1.5 text-center text-sm font-medium outline-none"
)}
onClick={handlePlaneOneModalOpen}
>
<Image src={PlaneOneLogo} alt="Plane One" width={24} height={24} />
{"Plane One"}
</button>
</Tooltip>
</>
);
return <PlaneOneEditionBadge />;
}
return (

View File

@@ -4,7 +4,7 @@ import { API_BASE_URL } from "@/helpers/common.helper";
// services
import { APIService } from "@/services/api.service";
export class DiscoService extends APIService {
export class PaymentService extends APIService {
constructor() {
super(API_BASE_URL);
}

View File

@@ -2,17 +2,17 @@ import { action, makeObservable, observable, runInAction } from "mobx";
// types
import { IWorkspaceProductSubscription } from "@plane/types";
// services
import { DiscoService } from "@/services/disco.service";
import { PaymentService } from "@/plane-web/services/payment.service";
const discoService = new DiscoService();
const paymentService = new PaymentService();
export interface IWorkspaceSubscriptionStore {
subscribedPlan: "FREE" | "PRO";
subscribedPlan: "FREE" | "PRO" | "ULTIMATE";
fetchWorkspaceSubscribedPlan: (workspaceSlug: string) => Promise<IWorkspaceProductSubscription>;
}
export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore {
subscribedPlan: "FREE" | "PRO" = "FREE";
subscribedPlan: "FREE" | "PRO" | "ULTIMATE" = "FREE";
constructor() {
makeObservable(this, {
@@ -23,9 +23,9 @@ export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore {
fetchWorkspaceSubscribedPlan = async (workspaceSlug: string) => {
try {
const response = await discoService.getWorkspaceCurrentPlane(workspaceSlug);
const response = await paymentService.getWorkspaceCurrentPlane(workspaceSlug);
runInAction(() => {
this.subscribedPlan = response.product;
this.subscribedPlan = response?.product || "FREE";
});
return response;
} catch (error) {