From bb7dbc6900f7c8e4a3ddcc2487f942fe00d2f7bf Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:37:31 +0530 Subject: [PATCH 1/4] fix: current plan api and return subscription id (#860) * fix: workspace current plan endpoint * dev: return workspace license * dev: add subscription details in user workspace website endpoint * chore: handled validation for subscription --------- Co-authored-by: gurusainath --- apiserver/plane/app/views/workspace/invite.py | 5 +- apiserver/plane/app/views/workspace/member.py | 3 - .../0006_workspacelicense_subscription.py | 18 ++ apiserver/plane/ee/models/workspace.py | 2 + .../plane/payment/bgtasks/member_sync_task.py | 75 +----- .../utils/workspace_license_request.py | 128 +++++++++++ apiserver/plane/payment/views/product.py | 217 +----------------- apiserver/plane/payment/views/subscription.py | 91 +------- packages/types/src/payment.d.ts | 1 + web/app/upgrade/pro/cloud/page.tsx | 1 + web/ee/components/license/cloud-badge.tsx | 6 + .../store/subscription/subscription.store.ts | 1 + web/ee/types/workspace.d.ts | 1 + 13 files changed, 174 insertions(+), 375 deletions(-) create mode 100644 apiserver/plane/ee/migrations/0006_workspacelicense_subscription.py diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py index a8178422a7..3695845886 100644 --- a/apiserver/plane/app/views/workspace/invite.py +++ b/apiserver/plane/app/views/workspace/invite.py @@ -7,7 +7,7 @@ import jwt from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import validate_email -from django.db.models import Count, F, Q +from django.db.models import Count from django.utils import timezone # Third party modules @@ -33,9 +33,6 @@ from plane.db.models import ( from plane.utils.cache import invalidate_cache, invalidate_cache_directly from plane.payment.bgtasks.member_sync_task import member_sync_task from .. import BaseViewSet -from plane.payment.utils.member_payment_count import ( - workspace_member_check, -) class WorkspaceInvitationsViewset(BaseViewSet): diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index 47a7f3ea31..8cc9dcbe86 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -36,9 +36,6 @@ from plane.db.models import ( ) from plane.utils.cache import cache_response, invalidate_cache from plane.payment.bgtasks.member_sync_task import member_sync_task -from plane.payment.utils.member_payment_count import ( - workspace_member_check, -) from .. import BaseViewSet diff --git a/apiserver/plane/ee/migrations/0006_workspacelicense_subscription.py b/apiserver/plane/ee/migrations/0006_workspacelicense_subscription.py new file mode 100644 index 0000000000..e212dba8a0 --- /dev/null +++ b/apiserver/plane/ee/migrations/0006_workspacelicense_subscription.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.14 on 2024-08-18 15:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ee', '0005_workspacelicense_has_activated_free_trial_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='workspacelicense', + name='subscription', + field=models.CharField(blank=True, max_length=255, null=True), + ), + ] diff --git a/apiserver/plane/ee/models/workspace.py b/apiserver/plane/ee/models/workspace.py index 3529d53a59..b402ecfa07 100644 --- a/apiserver/plane/ee/models/workspace.py +++ b/apiserver/plane/ee/models/workspace.py @@ -55,6 +55,8 @@ class WorkspaceLicense(BaseModel): has_activated_free_trial = models.BooleanField(default=False) # is payment method added has_added_payment_method = models.BooleanField(default=False) + # subscription + subscription = models.CharField(max_length=255, null=True, blank=True) class Meta: verbose_name = "Workspace License" diff --git a/apiserver/plane/payment/bgtasks/member_sync_task.py b/apiserver/plane/payment/bgtasks/member_sync_task.py index 6e8a91822f..2d33b063f0 100644 --- a/apiserver/plane/payment/bgtasks/member_sync_task.py +++ b/apiserver/plane/payment/bgtasks/member_sync_task.py @@ -4,17 +4,15 @@ import requests # Django imports from django.conf import settings from django.db.models import F -from django.utils import timezone # Third party imports from celery import shared_task # Module imports from plane.db.models import WorkspaceMember, Workspace -from plane.ee.models import WorkspaceLicense from plane.utils.exception_logger import log_exception from plane.payment.utils.workspace_license_request import ( - fetch_workspace_license, + resync_workspace_license, ) @@ -63,76 +61,7 @@ def member_sync_task(slug): response.raise_for_status() # Refresh workspace license - updated_workspace_license = fetch_workspace_license( - workspace_id=workspace_id, - workspace_slug=slug, - free_seats=WorkspaceMember.objects.filter( - is_active=True, - workspace__slug=slug, - member__is_bot=False, - ).count(), - ) - - # Update workspace license - workspace_license = WorkspaceLicense.objects.filter( - workspace=workspace - ).first() - - # If workspace license exists, update it - if workspace_license: - workspace_license.plane = updated_workspace_license["plan"] - workspace_license.recurring_interval = ( - updated_workspace_license["interval"] - ) - workspace_license.current_period_end_date = ( - updated_workspace_license["current_period_end_date"] - ) - workspace_license.purchased_seats = updated_workspace_license[ - "purchased_seats" - ] - workspace_license.free_seats = updated_workspace_license[ - "free_seats" - ] - workspace_license.is_cancelled = updated_workspace_license[ - "is_cancelled" - ] - workspace_license.is_offline_payment = ( - updated_workspace_license["is_offline_payment"] - ) - workspace_license.trial_end_date = updated_workspace_license[ - "trial_end_date" - ] - workspace_license.has_activated_free_trial = ( - updated_workspace_license["has_activated_free_trial"] - ) - workspace_license.has_added_payment_method = ( - updated_workspace_license["has_added_payment_method"] - ) - workspace_license.last_synced_at = timezone.now() - workspace_license.save() - - # Else create a new workspace license - else: - WorkspaceLicense.objects.create( - workspace=workspace, - plan=updated_workspace_license["plan"], - recurring_interval=updated_workspace_license["interval"], - current_period_end_date=updated_workspace_license[ - "current_period_end_date" - ], - purchased_seats=updated_workspace_license["seats"], - is_cancelled=updated_workspace_license["is_cancelled"], - is_offline_payment=updated_workspace_license[ - "is_offline_payment" - ], - last_synced_at=timezone.now(), - has_activated_free_trial=updated_workspace_license[ - "has_activated_free_trial" - ], - has_added_payment_method=updated_workspace_license[ - "has_added_payment_method" - ], - ) + resync_workspace_license(slug, force=True) else: return except requests.exceptions.RequestException as e: diff --git a/apiserver/plane/payment/utils/workspace_license_request.py b/apiserver/plane/payment/utils/workspace_license_request.py index 591e1de7f9..e0967f91da 100644 --- a/apiserver/plane/payment/utils/workspace_license_request.py +++ b/apiserver/plane/payment/utils/workspace_license_request.py @@ -3,6 +3,11 @@ import requests # Django imports from django.conf import settings +from django.utils import timezone + +# Module imports +from plane.db.models import Workspace, WorkspaceMember +from plane.ee.models import WorkspaceLicense def fetch_workspace_license(workspace_id, workspace_slug, free_seats=12): @@ -24,3 +29,126 @@ def fetch_workspace_license(workspace_id, workspace_slug, free_seats=12): response.raise_for_status() response = response.json() return response + + +def resync_workspace_license(workspace_slug, force=False): + # Fetch the workspace + workspace = Workspace.objects.get(slug=workspace_slug) + + # Check if the license is present for the workspace + workspace_license = WorkspaceLicense.objects.filter( + workspace=workspace + ).first() + + # If the license is present, then check if the last sync is more than 1 hour + if workspace_license: + # If the last sync is more than 1 hour, then sync the license + if ( + workspace_license.last_synced_at - timezone.now() + ).seconds > 3600 or force: + + # Fetch the workspace license + response = fetch_workspace_license( + workspace_id=str(workspace.id), + workspace_slug=workspace_slug, + free_seats=WorkspaceMember.objects.filter( + is_active=True, + workspace__slug=workspace_slug, + member__is_bot=False, + ).count(), + ) + # Update the last synced time + workspace_license.last_synced_at = timezone.now() + workspace_license.is_cancelled = response.get( + "is_cancelled", False + ) + workspace_license.free_seats = response.get("free_seats", 12) + workspace_license.purchased_seats = response.get( + "purchased_seats", 0 + ) + workspace_license.current_period_end_date = response.get( + "current_period_end_date" + ) + workspace_license.recurring_interval = response.get("interval") + workspace_license.plan = response.get("plan") + workspace_license.is_offline_payment = response.get( + "is_offline_payment", False + ) + workspace_license.trial_end_date = response.get("trial_end_date") + workspace_license.has_activated_free_trial = response.get( + "has_activated_free_trial", False + ) + workspace_license.has_added_payment_method = response.get( + "has_added_payment_method", False + ) + workspace_license.subscription = response.get("subscription") + workspace_license.save() + return { + "is_cancelled": workspace_license.is_cancelled, + "purchased_seats": workspace_license.purchased_seats, + "current_period_end_date": workspace_license.current_period_end_date, + "interval": workspace_license.recurring_interval, + "product": workspace_license.plan, + "is_offline_payment": workspace_license.is_offline_payment, + "trial_end_date": workspace_license.trial_end_date, + "has_activated_free_trial": workspace_license.has_activated_free_trial, + "has_added_payment_method": workspace_license.has_added_payment_method, + "subscription": workspace_license.subscription, + } + else: + return { + "is_cancelled": workspace_license.is_cancelled, + "purchased_seats": workspace_license.purchased_seats, + "current_period_end_date": workspace_license.current_period_end_date, + "interval": workspace_license.recurring_interval, + "product": workspace_license.plan, + "is_offline_payment": workspace_license.is_offline_payment, + "trial_end_date": workspace_license.trial_end_date, + "has_activated_free_trial": workspace_license.has_activated_free_trial, + "has_added_payment_method": workspace_license.has_added_payment_method, + "subscription": workspace_license.subscription, + } + # If the license is not present, then fetch the license from the payment server and create it + else: + # Fetch the workspace license + response = fetch_workspace_license( + workspace_id=str(workspace.id), + workspace_slug=workspace_slug, + free_seats=WorkspaceMember.objects.filter( + is_active=True, + workspace__slug=workspace_slug, + member__is_bot=False, + ).count(), + ) + # Create the workspace license + workspace_license = WorkspaceLicense.objects.create( + workspace=workspace, + is_cancelled=response.get("is_cancelled", False), + purchased_seats=response.get("purchased_seats", 0), + free_seats=response.get("free_seats", 12), + current_period_end_date=response.get("current_period_end_date"), + recurring_interval=response.get("interval"), + plan=response.get("plan"), + last_synced_at=timezone.now(), + trial_end_date=response.get("trial_end_date"), + has_activated_free_trial=response.get( + "has_activated_free_trial", False + ), + has_added_payment_method=response.get( + "has_added_payment_method", False + ), + subscription=response.get("subscription"), + ) + # Return the workspace license + return { + "is_cancelled": workspace_license.is_cancelled, + "purchased_seats": workspace_license.purchased_seats, + "current_period_end_date": workspace_license.current_period_end_date, + "interval": workspace_license.recurring_interval, + "product": workspace_license.plan, + "is_offline_payment": workspace_license.is_offline_payment, + "trial_end_date": workspace_license.trial_end_date, + "has_activated_free_trial": workspace_license.has_activated_free_trial, + "has_added_payment_method": workspace_license.has_added_payment_method, + "subscription": workspace_license.subscription, + } diff --git a/apiserver/plane/payment/views/product.py b/apiserver/plane/payment/views/product.py index 0ccc32501e..ad100a3192 100644 --- a/apiserver/plane/payment/views/product.py +++ b/apiserver/plane/payment/views/product.py @@ -3,8 +3,6 @@ import requests # Django imports from django.conf import settings -from django.db.models import CharField -from django.db.models.functions import Cast from django.utils import timezone from django.db.models import F @@ -18,11 +16,11 @@ from .base import BaseAPIView from plane.app.permissions.workspace import ( WorkspaceUserPermission, ) -from plane.db.models import WorkspaceMember, Workspace +from plane.db.models import WorkspaceMember from plane.ee.models import WorkspaceLicense from plane.utils.exception_logger import log_exception from plane.payment.utils.workspace_license_request import ( - fetch_workspace_license, + resync_workspace_license, ) @@ -119,6 +117,7 @@ class WebsiteUserWorkspaceEndpoint(BaseAPIView): "has_added_payment_method", "current_period_end_date", "is_offline_payment", + "subscription", ) ) @@ -149,137 +148,9 @@ class WorkspaceProductEndpoint(BaseAPIView): def get(self, request, slug): try: if settings.PAYMENT_SERVER_BASE_URL: - workspace = Workspace.objects.get(slug=slug) - - # Check if the license is present for the workspace - workspace_license = WorkspaceLicense.objects.filter( - workspace=workspace - ).first() - - # If the license is present, then check if the last sync is more than 1 hour - if workspace_license: - # If the last sync is more than 1 hour, then sync the license - if ( - workspace_license.last_synced_at - timezone.now() - ).seconds > 3600: - response = fetch_workspace_license( - workspace_id=str(workspace.id), - workspace_slug=slug, - free_seats=WorkspaceMember.objects.filter( - is_active=True, - workspace__slug=slug, - member__is_bot=False, - ).count(), - ) - - # Update the last synced time - workspace_license.last_synced_at = timezone.now() - workspace_license.is_cancelled = response.get( - "is_cancelled", False - ) - workspace_license.free_seats = response.get( - "free_seats", 12 - ) - workspace_license.purchased_seats = response.get( - "purchased_seats", 0 - ) - workspace_license.current_period_end_date = ( - response.get("current_period_end_date") - ) - workspace_license.recurring_interval = response.get( - "interval" - ) - workspace_license.plan = response.get("plan") - workspace_license.is_offline_payment = response.get( - "is_offline_payment", False - ) - workspace_license.trial_end_date = response.get( - "trial_end_date" - ) - workspace_license.has_activated_free_trial = ( - response.get("has_activated_free_trial", False) - ) - workspace_license.has_added_payment_method = ( - response.get("has_added_payment_method", False) - ) - workspace_license.save() - - return Response( - { - "is_cancelled": workspace_license.is_cancelled, - "purchased_seats": workspace_license.purchased_seats, - "current_period_end_date": workspace_license.current_period_end_date, - "interval": workspace_license.recurring_interval, - "product": workspace_license.plan, - "is_offline_payment": workspace_license.is_offline_payment, - "trial_end_date": workspace_license.trial_end_date, - "has_activated_free_trial": workspace_license.has_activated_free_trial, - "has_added_payment_method": workspace_license.has_added_payment_method, - }, - status=status.HTTP_200_OK, - ) - else: - return Response( - { - "is_cancelled": workspace_license.is_cancelled, - "purchased_seats": workspace_license.purchased_seats, - "current_period_end_date": workspace_license.current_period_end_date, - "interval": workspace_license.recurring_interval, - "product": workspace_license.plan, - "is_offline_payment": workspace_license.is_offline_payment, - "trial_end_date": workspace_license.trial_end_date, - "has_activated_free_trial": workspace_license.has_activated_free_trial, - "has_added_payment_method": workspace_license.has_added_payment_method, - }, - status=status.HTTP_200_OK, - ) - # If the license is not present, then fetch the license from the payment server and create it - else: - # Fetch the workspace license - response = fetch_workspace_license( - workspace_id=str(workspace.id), - workspace_slug=slug, - free_seats=WorkspaceMember.objects.filter( - is_active=True, - workspace__slug=slug, - member__is_bot=False, - ).count(), - ) - # Create the workspace license - workspace_license = WorkspaceLicense.objects.create( - workspace=workspace, - is_cancelled=response.get("is_cancelled", False), - purchased_seats=response.get("purchased_seats", 0), - free_seats=response.get("free_seats", 12), - current_period_end_date=response.get( - "current_period_end_date" - ), - recurring_interval=response.get("interval"), - plan=response.get("plan"), - last_synced_at=timezone.now(), - trial_end_date=response.get("trial_end_date"), - has_activated_free_trial=response.get( - "has_activated_free_trial", False - ), - has_added_payment_method=response.get( - "has_added_payment_method", False - ), - ) - # Return the workspace license - return Response( - { - "is_cancelled": workspace_license.is_cancelled, - "purchased_seats": workspace_license.purchased_seats, - "current_period_end_date": workspace_license.current_period_end_date, - "interval": workspace_license.recurring_interval, - "product": workspace_license.plan, - "is_offline_payment": workspace_license.is_offline_payment, - "trial_end_date": workspace_license.trial_end_date, - "has_activated_free_trial": workspace_license.has_activated_free_trial, - "has_added_payment_method": workspace_license.has_added_payment_method, - }, - status=status.HTTP_200_OK, - ) + # Resync the workspace license + response = resync_workspace_license(workspace_slug=slug) + return Response(response, status=status.HTTP_200_OK) else: return Response( {"error": "error fetching product details"}, @@ -295,84 +166,16 @@ class WorkspaceProductEndpoint(BaseAPIView): class WorkspaceLicenseRefreshEndpoint(BaseAPIView): def post(self, request, slug): - workspace = Workspace.objects.get(slug=slug) - - # Check if the license is present for the workspace - workspace_license = WorkspaceLicense.objects.filter( - workspace=workspace - ).first() - - # If the license is present, then fetch the license from the payment server and update it - if workspace_license: - # Update the values in the workspace license - response = fetch_workspace_license( - workspace_id=str(workspace.id), - workspace_slug=slug, - free_seats=WorkspaceMember.objects.filter( - is_active=True, - workspace__slug=slug, - member__is_bot=False, - ).count(), - ) - workspace_license.is_cancelled = response.get( - "is_cancelled", False - ) - workspace_license.free_seats = response.get("free_seats", 12) - workspace_license.purchased_seats = response.get( - "purchased_seats", 0 - ) - workspace_license.current_period_end_date = response.get( - "current_period_end_date" - ) - workspace_license.recurring_interval = response.get("interval") - workspace_license.plan = response.get("plan") - workspace_license.trial_end_date = response.get("trial_end_date") - workspace_license.has_activated_free_trial = response.get( - "has_activated_free_trial", False - ) - workspace_license.has_added_payment_method = response.get( - "has_added_payment_method", False - ) - workspace_license.last_synced_at = timezone.now() - workspace_license.save() - # If the license is not present, then fetch the license from the payment server and create it - else: - # Fetch the workspace license - response = fetch_workspace_license( - workspace_id=str(workspace.id), - workspace_slug=slug, - free_seats=WorkspaceMember.objects.filter( - is_active=True, - workspace__slug=slug, - member__is_bot=False, - ).count(), - ) - # Create the workspace license - workspace_license = WorkspaceLicense.objects.create( - workspace=workspace, - is_cancelled=response.get("is_cancelled", False), - purchased_seats=response.get("purchased_seats", 0), - free_seats=response.get("free_seats", 12), - current_period_end_date=response.get( - "current_period_end_date" - ), - recurring_interval=response.get("interval"), - plan=response.get("plan"), - last_synced_at=timezone.now(), - trial_end_date=response.get("trial_end_date"), - has_activated_free_trial=response.get( - "has_activated_free_trial", False - ), - has_added_payment_method=response.get( - "has_added_payment_method", False - ), - ) + # Resync the workspace license + _ = resync_workspace_license(workspace_slug=slug, force=True) # Return the response return Response(status=status.HTTP_204_NO_CONTENT) class WorkspaceLicenseSyncEndpoint(BaseAPIView): + """This endpoint is used to sync the workspace license from the payment server""" + permission_classes = [ AllowAny, ] diff --git a/apiserver/plane/payment/views/subscription.py b/apiserver/plane/payment/views/subscription.py index b47e27ef7c..b96e7891a4 100644 --- a/apiserver/plane/payment/views/subscription.py +++ b/apiserver/plane/payment/views/subscription.py @@ -3,7 +3,6 @@ import requests # Django imports from django.conf import settings -from django.utils import timezone # Third party imports from rest_framework import status @@ -12,11 +11,10 @@ from rest_framework.response import Response # Module imports from .base import BaseAPIView from plane.app.permissions.workspace import WorkspaceOwnerPermission -from plane.db.models import Workspace, WorkspaceMember -from plane.ee.models import WorkspaceLicense +from plane.db.models import Workspace from plane.utils.exception_logger import log_exception from plane.payment.utils.workspace_license_request import ( - fetch_workspace_license, + resync_workspace_license, ) @@ -102,90 +100,7 @@ class UpgradeSubscriptionEndpoint(BaseAPIView): response.raise_for_status() # Refetch the workspace license - workspace_license = WorkspaceLicense.objects.filter( - workspace=workspace - ).first() - - # Refetch the workspace license - workspace_license_response = fetch_workspace_license( - workspace_id=str(workspace.id), - workspace_slug=slug, - free_seats=WorkspaceMember.objects.filter( - is_active=True, - workspace__slug=slug, - member__is_bot=False, - ).count(), - ) - - if workspace_license: - # Update the last synced time - workspace_license.last_synced_at = timezone.now() - workspace_license.is_cancelled = ( - workspace_license_response.get("is_cancelled", False) - ) - workspace_license.purchased_seats = ( - workspace_license_response.get("purchased_seats", 0) - ) - workspace_license.free_seats = ( - workspace_license_response.get("free_seats", 12) - ) - workspace_license.current_period_end_date = ( - workspace_license_response.get( - "current_period_end_date" - ) - ) - workspace_license.recurring_interval = ( - workspace_license_response.get("interval") - ) - workspace_license.plan = workspace_license_response.get( - "plan" - ) - workspace_license.trial_end_date = ( - workspace_license_response.get("trial_end_date") - ) - workspace_license.has_activated_free_trial = ( - workspace_license_response.get( - "has_activated_free_trial" - ) - ) - workspace_license.has_added_payment_method = ( - workspace_license_response.get( - "has_added_payment_method" - ) - ) - workspace_license.save() - else: - # Create a new workspace license - WorkspaceLicense.objects.create( - workspace=workspace, - last_synced_at=timezone.now(), - is_cancelled=workspace_license_response.get( - "is_cancelled", False - ), - purchased_seats=workspace_license_response.get( - "purchased_seats", 0 - ), - free_seats=workspace_license_response.get( - "free_seats", 12 - ), - current_period_end_date=workspace_license_response.get( - "current_period_end_date" - ), - recurring_interval=workspace_license_response.get( - "interval" - ), - trial_end_date=workspace_license_response.get( - "trial_end_date" - ), - plan=workspace_license_response.get("plan"), - has_activated_free_trial=workspace_license_response.get( - "has_activated_free_trial" - ), - has_added_payment_method=workspace_license_response.get( - "has_added_payment_method" - ), - ) - + resync_workspace_license(workspace_slug=workspace.slug) # Return the response return Response(status=status.HTTP_204_NO_CONTENT) return Response( diff --git a/packages/types/src/payment.d.ts b/packages/types/src/payment.d.ts index ff80a5a484..3a43cef2c0 100644 --- a/packages/types/src/payment.d.ts +++ b/packages/types/src/payment.d.ts @@ -27,4 +27,5 @@ export type IWorkspaceProductSubscription = { purchased_seats: number | undefined; has_activated_free_trial: boolean; has_added_payment_method: boolean; + subscription: string | undefined; }; diff --git a/web/app/upgrade/pro/cloud/page.tsx b/web/app/upgrade/pro/cloud/page.tsx index 5945045156..c90c4db271 100644 --- a/web/app/upgrade/pro/cloud/page.tsx +++ b/web/app/upgrade/pro/cloud/page.tsx @@ -72,6 +72,7 @@ const CloudUpgradePage = observer(() => { }; const workspaceSubscription = (workspace: TWorkspaceWithProductDetails): "FREE" | "PRO" | "TRIAL" => { + if (!workspace.subscription) return "FREE"; switch (workspace.product) { case "PRO": if (workspace.is_offline_payment) { diff --git a/web/ee/components/license/cloud-badge.tsx b/web/ee/components/license/cloud-badge.tsx index 87eaba9ee5..53b1f215b6 100644 --- a/web/ee/components/license/cloud-badge.tsx +++ b/web/ee/components/license/cloud-badge.tsx @@ -74,6 +74,9 @@ export const CloudEditionBadge = observer(() => { subscriptionDetail?.has_activated_free_trial && !subscriptionDetail?.trial_end_date ? true : false; const renderButtonText = () => { + if (!subscriptionDetail.subscription) { + return "Upgrade to Pro"; + } switch (subscriptionDetail.product) { case "FREE": { if (subscriptionDetail?.has_activated_free_trial) { @@ -98,6 +101,9 @@ export const CloudEditionBadge = observer(() => { }; const showPaymentButton = () => { + if (!subscriptionDetail.subscription) { + return true; + } switch (subscriptionDetail.product) { case "FREE": { return true; diff --git a/web/ee/store/subscription/subscription.store.ts b/web/ee/store/subscription/subscription.store.ts index a6a8a16c59..12695880c5 100644 --- a/web/ee/store/subscription/subscription.store.ts +++ b/web/ee/store/subscription/subscription.store.ts @@ -63,6 +63,7 @@ export class WorkspaceSubscriptionStore implements IWorkspaceSubscriptionStore { purchased_seats: response?.purchased_seats || 0, has_activated_free_trial: response?.has_activated_free_trial || false, has_added_payment_method: response?.has_added_payment_method || false, + subscription: response?.subscription || undefined, }); }); return response; diff --git a/web/ee/types/workspace.d.ts b/web/ee/types/workspace.d.ts index cc9476252c..85f4803aa8 100644 --- a/web/ee/types/workspace.d.ts +++ b/web/ee/types/workspace.d.ts @@ -12,4 +12,5 @@ export type TWorkspaceWithProductDetails = { has_added_payment_method: boolean; trial_end_date: string; is_offline_payment: boolean; + subscription: string; }; From 99ee309e6a233c4aab1de1c77ad31a0a6d045480 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 19 Aug 2024 14:19:57 +0530 Subject: [PATCH 2/4] fix: list products enpoint not being called. (#864) --- web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx | 2 +- web/ee/components/license/cloud-badge.tsx | 7 ------- web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx | 2 +- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx b/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx index 811ae7a3d8..2e93767461 100644 --- a/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx +++ b/web/app/upgrade/pro/cloud/[selectedWorkspace]/page.tsx @@ -48,7 +48,7 @@ const CloudUpgradePlanPage = observer(() => { // fetch products const { data: products, isLoading: isLoadingProduct } = useSWR( - workspaceSlug ? "CLOUD_PRO_PRODUCTS" : null, + workspaceSlug ? `CLOUD_PRO_PRODUCTS_${workspaceSlug?.toString()}` : null, workspaceSlug ? () => paymentService.listProducts(workspaceSlug.toString()) : null, { errorRetryCount: 2, diff --git a/web/ee/components/license/cloud-badge.tsx b/web/ee/components/license/cloud-badge.tsx index 53b1f215b6..57995adb15 100644 --- a/web/ee/components/license/cloud-badge.tsx +++ b/web/ee/components/license/cloud-badge.tsx @@ -67,12 +67,6 @@ export const CloudEditionBadge = observer(() => { ); - // derived values - const isInTrialPeriod = - subscriptionDetail?.has_activated_free_trial && subscriptionDetail?.trial_end_date ? true : false; - const isTrialCompleted = - subscriptionDetail?.has_activated_free_trial && !subscriptionDetail?.trial_end_date ? true : false; - const renderButtonText = () => { if (!subscriptionDetail.subscription) { return "Upgrade to Pro"; @@ -140,7 +134,6 @@ export const CloudEditionBadge = observer(() => { handleClose={() => toggleProPlanModal(false)} yearlyPlan={false} handleSuccessModal={() => setProPlanSuccessModalOpen(true)} - canFetchProducts={subscriptionDetail.product === "FREE" || isInTrialPeriod || isTrialCompleted} /> {showPaymentButton() && ( 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 7fe30f033e..a1eedb2084 100644 --- a/web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx +++ b/web/ee/components/license/pro-plan-cloud-upgrade-modal.tsx @@ -57,7 +57,7 @@ export const ProPlanCloudUpgradeModal: FC = (prop const isAdmin = currentWorkspaceRole === EUserWorkspaceRoles.ADMIN; // fetch products const { isLoading: isProductsAPILoading, data } = useSWR( - workspaceSlug && canFetchProducts ? "CLOUD_PAYMENT_PRODUCTS" : null, + workspaceSlug && canFetchProducts ? `CLOUD_PAYMENT_PRODUCTS_${workspaceSlug?.toString()}` : null, workspaceSlug && canFetchProducts ? () => paymentService.listProducts(workspaceSlug.toString()) : null, { errorRetryCount: 2, From 0f9cb41a85904bb9520a93edba63440865ab56ad Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:09:14 +0530 Subject: [PATCH 3/4] fix: license sync endpoint (#866) * dev: update licence subscription sync * fix: refresh interval on workspace license * chore: display trial end date on billing page. --------- Co-authored-by: Prateek Shourya --- .../utils/workspace_license_request.py | 2 +- apiserver/plane/payment/views/product.py | 2 ++ .../license/plane-cloud-billing.tsx | 30 ++++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/payment/utils/workspace_license_request.py b/apiserver/plane/payment/utils/workspace_license_request.py index e0967f91da..06a9fe2e5c 100644 --- a/apiserver/plane/payment/utils/workspace_license_request.py +++ b/apiserver/plane/payment/utils/workspace_license_request.py @@ -45,7 +45,7 @@ def resync_workspace_license(workspace_slug, force=False): # If the last sync is more than 1 hour, then sync the license if ( workspace_license.last_synced_at - timezone.now() - ).seconds > 3600 or force: + ).total_seconds() > 3600 or force: # Fetch the workspace license response = fetch_workspace_license( diff --git a/apiserver/plane/payment/views/product.py b/apiserver/plane/payment/views/product.py index ad100a3192..0b89a7c8f9 100644 --- a/apiserver/plane/payment/views/product.py +++ b/apiserver/plane/payment/views/product.py @@ -230,6 +230,7 @@ class WorkspaceLicenseSyncEndpoint(BaseAPIView): workspace_license.has_added_payment_method = request.data.get( "has_added_payment_method", False ) + workspace_license.subscription = request.data.get("subscription") workspace_license.save() # If the workspace license is not present, then fetch the license from the payment server and create it else: @@ -252,6 +253,7 @@ class WorkspaceLicenseSyncEndpoint(BaseAPIView): has_added_payment_method=request.data.get( "has_added_payment_method", False ), + subscription=request.data.get("subscription"), ) # Return the response diff --git a/web/ee/components/license/plane-cloud-billing.tsx b/web/ee/components/license/plane-cloud-billing.tsx index b1ea1c6740..fe895e2c19 100644 --- a/web/ee/components/license/plane-cloud-billing.tsx +++ b/web/ee/components/license/plane-cloud-billing.tsx @@ -26,6 +26,9 @@ export const PlaneCloudBilling: React.FC = observer(() => { // derived values const endDate = currentWorkspaceSubscribedPlanDetail?.current_period_end_date; const isSubscriptionCancelled = currentWorkspaceSubscribedPlanDetail?.is_canceled; + const isInTrialPeriod = + !currentWorkspaceSubscribedPlanDetail?.has_added_payment_method && + !!currentWorkspaceSubscribedPlanDetail?.trial_end_date; const handleSubscriptionPageRedirection = () => { setIsLoading(true); @@ -102,15 +105,26 @@ export const PlaneCloudBilling: React.FC = observer(() => {
Plane pro

Plane Pro

- {isSubscriptionCancelled ? ( -
- (Expires on: {renderFormattedDate(endDate)}) -
- ) : ( -
- (Renew on: {renderFormattedDate(endDate)}) -
+ {isInTrialPeriod && ( + <> +
+ Free Trial +
+
+ (Free trial ends on: {renderFormattedDate(currentWorkspaceSubscribedPlanDetail?.trial_end_date)}) +
+ )} + {!isInTrialPeriod && + (isSubscriptionCancelled ? ( +
+ (Expires on: {renderFormattedDate(endDate)}) +
+ ) : ( +
+ (Renew on: {renderFormattedDate(endDate)}) +
+ ))}
{!currentWorkspaceSubscribedPlanDetail.is_offline_payment && (
From 647dc4d0ad72308d9cf0eb301a32814ba6db9b03 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Mon, 19 Aug 2024 19:28:18 +0530 Subject: [PATCH 4/4] fix: minor feature flags fixes. (#871) * fix: feature flag private views access switcher. * fix: show issue worklog as disabled on project settings if feature flag is inactive. --- .../components/project/settings/features-list.tsx | 2 +- web/ee/components/views/access-controller.tsx | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/web/core/components/project/settings/features-list.tsx b/web/core/components/project/settings/features-list.tsx index 540ad1f6f5..40eb189150 100644 --- a/web/core/components/project/settings/features-list.tsx +++ b/web/core/components/project/settings/features-list.tsx @@ -98,7 +98,7 @@ export const ProjectFeaturesList: FC = observer((props) => {
handleSubmit(featureItemKey, featureItem.property)} disabled={ !featureItem.isEnabled || !isAdmin || featureItem.property === "is_time_tracking_enabled" diff --git a/web/ee/components/views/access-controller.tsx b/web/ee/components/views/access-controller.tsx index 84c7de6f5e..024e21e07c 100644 --- a/web/ee/components/views/access-controller.tsx +++ b/web/ee/components/views/access-controller.tsx @@ -1,13 +1,25 @@ +import { useParams } from "next/navigation"; import { Control, Controller } from "react-hook-form"; +// types import { IProjectView, IWorkspaceView } from "@plane/types"; +// components import { AccessField } from "@/components/common/access-field"; +// constants import { VIEW_ACCESS_SPECIFIERS } from "@/constants/views"; +import { useFlag } from "@/plane-web/hooks/store"; type Props = { control: Control | Control; }; export const AccessController = (props: Props) => { const { control } = props; + // router + const { workspaceSlug } = useParams(); + // plane web hooks + const isPrivateViewsEnabled = useFlag(workspaceSlug?.toString(), "VIEW_ACCESS_PRIVATE"); + + if (!isPrivateViewsEnabled) return null; + return ( }