diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 0a59fa7b5..17923975d 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ organizationId: z.string().trim() }), + querystring: z.object({ + refreshCache: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }), response: { 200: z.object({ plan: z.any() }) } @@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - orgId: req.params.organizationId + orgId: req.params.organizationId, + refreshCache: req.query.refreshCache }); return { plan }; } diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index a3327b264..ca1615ce8 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -99,6 +99,17 @@ export const licenseServiceFactory = ({ const workspacesUsed = await projectDAL.countOfOrgProjects(null); currentPlan.workspacesUsed = workspacesUsed; + const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null); + if (usedIdentitySeats !== currentPlan.identitiesUsed) { + const usedSeats = await licenseDAL.countOfOrgMembers(null); + await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { + usedSeats, + usedIdentitySeats + }); + currentPlan.identitiesUsed = usedIdentitySeats; + currentPlan.membersUsed = usedSeats; + } + onPremFeatures = currentPlan; logger.info("Successfully synchronized license key features"); } catch (error) { @@ -226,10 +237,13 @@ export const licenseServiceFactory = ({ }; const refreshPlan = async (orgId: string) => { + await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); if (instanceType === InstanceType.Cloud) { - await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); await getPlan(orgId); } + if (instanceType === InstanceType.EnterpriseOnPrem) { + await syncLicenseKeyOnPremFeatures(true); + } }; const generateOrgCustomerId = async (orgName: string, email?: string | null) => { @@ -296,8 +310,19 @@ export const licenseServiceFactory = ({ return data; }; - const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => { + const getOrgPlan = async ({ + orgId, + actor, + actorId, + actorOrgId, + actorAuthMethod, + projectId, + refreshCache + }: TOrgPlanDTO) => { await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + if (refreshCache) { + await refreshPlan(orgId); + } const plan = await getPlan(orgId, projectId); return plan; }; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 345e26638..2ccd3ac8f 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -87,6 +87,7 @@ export type TOrgPlansTableDTO = { export type TOrgPlanDTO = { projectId?: string; + refreshCache?: boolean; } & TOrgPermission; export type TStartOrgTrialDTO = { diff --git a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx index de52e5f18..95da6bb01 100644 --- a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx +++ b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx @@ -3,7 +3,7 @@ import { useRouteContext } from "@tanstack/react-router"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; -export const useSubscription = () => { +export const useSubscription = (refreshCache?: boolean) => { const organizationId = useRouteContext({ from: "/_authenticate/_inject-org-details", select: (el) => el.organizationId @@ -11,7 +11,7 @@ export const useSubscription = () => { const { data: subscription } = useSuspenseQuery({ queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId), - queryFn: () => fetchOrgSubscription(organizationId), + queryFn: () => fetchOrgSubscription(organizationId, refreshCache), staleTime: Infinity }); diff --git a/frontend/src/hooks/api/subscriptions/queries.tsx b/frontend/src/hooks/api/subscriptions/queries.tsx index 99b1f4486..f545565eb 100644 --- a/frontend/src/hooks/api/subscriptions/queries.tsx +++ b/frontend/src/hooks/api/subscriptions/queries.tsx @@ -10,9 +10,9 @@ export const subscriptionQueryKeys = { getOrgSubsription: (orgID: string) => ["plan", { orgID }] as const }; -export const fetchOrgSubscription = async (orgID: string) => { +export const fetchOrgSubscription = async (orgID: string, refreshCache: boolean = false) => { const { data } = await apiRequest.get<{ plan: SubscriptionPlan }>( - `/api/v1/organizations/${orgID}/plan` + `/api/v1/organizations/${orgID}/plan${refreshCache ? "?refreshCache=true" : ""}` ); return data.plan; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 338599fe0..0c3733cc8 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -54,5 +54,7 @@ export type SubscriptionPlan = { secretScanning: boolean; enterpriseSecretSyncs: boolean; enterpriseAppConnections: boolean; + cardDeclined?: boolean; + cardDeclinedReason?: string; machineIdentityAuthTemplates: boolean; }; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 63da8b533..e9bb6a902 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons"; import { @@ -8,6 +8,7 @@ import { faCaretDown, faCheck, faEnvelope, + faExclamationTriangle, faInfo, faInfoCircle, faServer, @@ -111,6 +112,14 @@ export const Navbar = () => { const { subscription } = useSubscription(); const { currentOrg } = useOrganization(); const [showAdminsModal, setShowAdminsModal] = useState(false); + const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + + useEffect(() => { + if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) { + sessionStorage.setItem("paymentFailed", "true"); + setShowCardDeclinedModal(true); + } + }, [subscription]); const { data: orgs } = useGetOrganizations(); const navigate = useNavigate(); @@ -222,6 +231,19 @@ export const Navbar = () => {
+ We were unable to process your last payment + {subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. + Please update your payment information to continue using premium features. +
+