diff --git a/apiserver/plane/payment/views/payment.py b/apiserver/plane/payment/views/payment.py index 145f6afb69..d51045f5e3 100644 --- a/apiserver/plane/payment/views/payment.py +++ b/apiserver/plane/payment/views/payment.py @@ -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, diff --git a/apiserver/plane/payment/views/product.py b/apiserver/plane/payment/views/product.py index 6ad2510b45..c4d148e922 100644 --- a/apiserver/plane/payment/views/product.py +++ b/apiserver/plane/payment/views/product.py @@ -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, diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 3c8ce3906a..1c706ac485 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -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", "") \ No newline at end of file diff --git a/packages/types/src/payment.d.ts b/packages/types/src/payment.d.ts index b63f3c1ce5..1ddb340fc1 100644 --- a/packages/types/src/payment.d.ts +++ b/packages/types/src/payment.d.ts @@ -16,6 +16,6 @@ export type IPaymentProduct = { }; export type IWorkspaceProductSubscription = { - product: FREE | PRO | ULTIMATE; + product: "FREE" | "PRO" | "ULTIMATE"; expiry_date: string | null; }; diff --git a/turbo.json b/turbo.json index 88640aa5f0..e76d5718b2 100644 --- a/turbo.json +++ b/turbo.json @@ -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": { diff --git a/web/core/components/workspace/plane-badge.tsx b/web/core/components/workspace/plane-badge.tsx deleted file mode 100644 index e7213b48f8..0000000000 --- a/web/core/components/workspace/plane-badge.tsx +++ /dev/null @@ -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 ( - <> - setIsProPlanModalOpen(false)} /> - setProPlanDetailsModalOpen(false)} /> - {data && data.product === "s" && ( - - )} - {data && data.product === "FREE" && ( -
- - Pro - -
- )} - - ); - } - - if (instance?.product === "plane-one") { - return ( - <> - setIsPlaneOneModalOpen(false)} /> - - - - - ); - } - - return ( - <> - -
- Enterprise Edition -
-
- - ); -}); diff --git a/web/ee/components/license/cloud-badge.tsx b/web/ee/components/license/cloud-badge.tsx new file mode 100644 index 0000000000..f5904b0a61 --- /dev/null +++ b/web/ee/components/license/cloud-badge.tsx @@ -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 ( + <> + setIsProPlanModalOpen(false)} /> + setProPlanDetailsModalOpen(false)} /> + {subscribedPlan === "FREE" && ( + + )} + {subscribedPlan === "PRO" && ( +
+ + Pro + +
+ )} + + ); +}); diff --git a/web/ee/components/license/cloud-products-modal.tsx b/web/ee/components/license/cloud-products-modal.tsx index 71e4143f0b..d7bc1d69bc 100644 --- a/web/ee/components/license/cloud-products-modal.tsx +++ b/web/ee/components/license/cloud-products-modal.tsx @@ -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 = (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 = (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, diff --git a/web/ee/components/license/index.ts b/web/ee/components/license/index.ts index bc206253ee..1ebbcb9bd9 100644 --- a/web/ee/components/license/index.ts +++ b/web/ee/components/license/index.ts @@ -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"; diff --git a/web/ee/components/license/plane-one-badge.tsx b/web/ee/components/license/plane-one-badge.tsx new file mode 100644 index 0000000000..c2b0bdaefd --- /dev/null +++ b/web/ee/components/license/plane-one-badge.tsx @@ -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 ( + <> + setIsPlaneOneModalOpen(false)} /> + + + + + ); +}); diff --git a/web/ee/components/workspace/edition-badge.tsx b/web/ee/components/workspace/edition-badge.tsx index b182df2365..03ba242be8 100644 --- a/web/ee/components/workspace/edition-badge.tsx +++ b/web/ee/components/workspace/edition-badge.tsx @@ -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 ( - <> - setIsProPlanModalOpen(false)} /> - setProPlanDetailsModalOpen(false)} /> - {subscribedPlan === "FREE" && ( - - )} - {subscribedPlan === "PRO" && ( -
- - Pro - -
- )} - - ); + if (DISCO_BASE_URL.length > 0) { + return ; } if (instance?.product === "plane-one") { - return ( - <> - setIsPlaneOneModalOpen(false)} /> - - - - - ); + return ; } return ( diff --git a/web/core/services/disco.service.ts b/web/ee/services/payment.service.ts similarity index 95% rename from web/core/services/disco.service.ts rename to web/ee/services/payment.service.ts index 450db1af05..2d0ea9bae9 100644 --- a/web/core/services/disco.service.ts +++ b/web/ee/services/payment.service.ts @@ -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); } diff --git a/web/ee/store/subscription/subscription.store.ts b/web/ee/store/subscription/subscription.store.ts index 197ad80ec9..4873740e68 100644 --- a/web/ee/store/subscription/subscription.store.ts +++ b/web/ee/store/subscription/subscription.store.ts @@ -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; } 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) {