diff --git a/apps/web/modules/ee/license-check/lib/utils.test.ts b/apps/web/modules/ee/license-check/lib/utils.test.ts index 14c809fd08..9718546a6d 100644 --- a/apps/web/modules/ee/license-check/lib/utils.test.ts +++ b/apps/web/modules/ee/license-check/lib/utils.test.ts @@ -8,6 +8,7 @@ import { getBiggerUploadFileSizePermission, getIsContactsEnabled, getIsMultiOrgEnabled, + getIsQuotasEnabled, getIsSamlSsoEnabled, getIsSpamProtectionEnabled, getIsSsoEnabled, @@ -48,6 +49,7 @@ const defaultFeatures: TEnterpriseLicenseFeatures = { auditLogs: false, multiLanguageSurveys: false, accessControl: false, + quotas: false, }; const defaultLicense = { @@ -184,10 +186,10 @@ describe("License Utils", () => { expect(result).toBe(true); }); - test("should return true if license active but accessControl feature disabled because of fallback", async () => { + test("should return false if license active but accessControl feature disabled (self-hosted)", async () => { vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense); const result = await getAccessControlPermission(mockOrganization.billing.plan); - expect(result).toBe(true); + expect(result).toBe(false); }); test("should return false if license is inactive", async () => { @@ -273,10 +275,10 @@ describe("License Utils", () => { expect(result).toBe(true); }); - test("should return true if license active but multiLanguageSurveys feature disabled because of fallback", async () => { + test("should return false if license active but multiLanguageSurveys feature disabled (self-hosted)", async () => { vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense); const result = await getMultiLanguagePermission(mockOrganization.billing.plan); - expect(result).toBe(true); + expect(result).toBe(false); }); test("should return false if license is inactive", async () => { @@ -289,6 +291,54 @@ describe("License Utils", () => { }); }); + describe("getIsQuotasEnabled", () => { + test("should return true if license active and quotas feature enabled (self-hosted)", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = false; + vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({ + ...defaultLicense, + features: { ...defaultFeatures, quotas: true }, + }); + const result = await getIsQuotasEnabled(mockOrganization.billing.plan); + expect(result).toBe(true); + }); + + test("should return true if license active, quotas enabled and plan is CUSTOM (cloud)", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = true; + vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({ + ...defaultLicense, + features: { ...defaultFeatures, quotas: true }, + }); + const result = await getIsQuotasEnabled(constants.PROJECT_FEATURE_KEYS.CUSTOM); + expect(result).toBe(true); + }); + + test("should return false if license active, quotas enabled but plan is not CUSTOM (cloud)", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = true; + vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({ + ...defaultLicense, + features: { ...defaultFeatures, quotas: true }, + }); + const result = await getIsQuotasEnabled(constants.PROJECT_FEATURE_KEYS.STARTUP); + expect(result).toBe(false); + }); + + test("should return false if license active but quotas feature disabled (self-hosted)", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = false; + vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense); + const result = await getIsQuotasEnabled(mockOrganization.billing.plan); + expect(result).toBe(false); + }); + + test("should return false if license is inactive", async () => { + vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({ + ...defaultLicense, + active: false, + }); + const result = await getIsQuotasEnabled(mockOrganization.billing.plan); + expect(result).toBe(false); + }); + }); + describe("getIsMultiOrgEnabled", () => { test("should return true if feature flag isMultiOrgEnabled is true", async () => { vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({ diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts index 48c0a7c8ee..9563c808e8 100644 --- a/apps/web/modules/ee/license-check/lib/utils.ts +++ b/apps/web/modules/ee/license-check/lib/utils.ts @@ -10,6 +10,8 @@ import { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/types/ent import { getEnterpriseLicense, getLicenseFeatures } from "./license"; // Helper function for feature permissions (e.g., removeBranding, whitelabel) +// On Cloud: requires active license and non-FREE plan +// On Self-hosted: requires active license and feature enabled const getFeaturePermission = async ( billingPlan: Organization["billing"]["plan"], featureKey: keyof Pick @@ -23,6 +25,41 @@ const getFeaturePermission = async ( } }; +// Helper function for enterprise features that require CUSTOM plan on Cloud +// On Cloud: requires active license AND feature enabled in license AND CUSTOM billing plan +// On Self-hosted: requires active license AND feature enabled in license +const getCustomPlanFeaturePermission = async ( + billingPlan: Organization["billing"]["plan"], + featureKey: keyof Pick +): Promise => { + const license = await getEnterpriseLicense(); + + if (!license.active) return false; + + const isFeatureEnabled = license.features?.[featureKey] ?? false; + if (!isFeatureEnabled) return false; + + if (IS_FORMBRICKS_CLOUD) { + return billingPlan === PROJECT_FEATURE_KEYS.CUSTOM; + } + + return true; +}; + +// Helper function for license-only feature flags (no billing plan check) +// Returns true only if the license is active AND the specific feature is enabled in the license +// Used for features that are controlled purely by the license key, not billing plans +const getSpecificFeatureFlag = async ( + featureKey: keyof Pick< + TEnterpriseLicenseFeatures, + "isMultiOrgEnabled" | "contacts" | "twoFactorAuth" | "sso" | "auditLogs" + > +): Promise => { + const licenseFeatures = await getLicenseFeatures(); + if (!licenseFeatures) return false; + return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false; +}; + export const getRemoveBrandingPermission = async ( billingPlan: Organization["billing"]["plan"] ): Promise => { @@ -45,24 +82,6 @@ export const getBiggerUploadFileSizePermission = async ( return false; }; -const getSpecificFeatureFlag = async ( - featureKey: keyof Pick< - TEnterpriseLicenseFeatures, - | "isMultiOrgEnabled" - | "contacts" - | "twoFactorAuth" - | "sso" - | "auditLogs" - | "multiLanguageSurveys" - | "accessControl" - | "quotas" - > -): Promise => { - const licenseFeatures = await getLicenseFeatures(); - if (!licenseFeatures) return false; - return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false; -}; - export const getIsMultiOrgEnabled = async (): Promise => { return getSpecificFeatureFlag("isMultiOrgEnabled"); }; @@ -80,12 +99,7 @@ export const getIsSsoEnabled = async (): Promise => { }; export const getIsQuotasEnabled = async (billingPlan: Organization["billing"]["plan"]): Promise => { - const isEnabled = await getSpecificFeatureFlag("quotas"); - // If the feature is enabled in the license, return true - if (isEnabled) return true; - - // If the feature is not enabled in the license, check the fallback(Backwards compatibility) - return featureFlagFallback(billingPlan); + return getCustomPlanFeaturePermission(billingPlan, "quotas"); }; export const getIsAuditLogsEnabled = async (): Promise => { @@ -118,33 +132,16 @@ export const getIsSpamProtectionEnabled = async ( return license.active && !!license.features?.spamProtection; }; -const featureFlagFallback = async (billingPlan: Organization["billing"]["plan"]): Promise => { - const license = await getEnterpriseLicense(); - if (IS_FORMBRICKS_CLOUD) return license.active && billingPlan === PROJECT_FEATURE_KEYS.CUSTOM; - else if (!IS_FORMBRICKS_CLOUD) return license.active; - return false; -}; - export const getMultiLanguagePermission = async ( billingPlan: Organization["billing"]["plan"] ): Promise => { - const isEnabled = await getSpecificFeatureFlag("multiLanguageSurveys"); - // If the feature is enabled in the license, return true - if (isEnabled) return true; - - // If the feature is not enabled in the license, check the fallback(Backwards compatibility) - return featureFlagFallback(billingPlan); + return getCustomPlanFeaturePermission(billingPlan, "multiLanguageSurveys"); }; export const getAccessControlPermission = async ( billingPlan: Organization["billing"]["plan"] ): Promise => { - const isEnabled = await getSpecificFeatureFlag("accessControl"); - // If the feature is enabled in the license, return true - if (isEnabled) return true; - - // If the feature is not enabled in the license, check the fallback(Backwards compatibility) - return featureFlagFallback(billingPlan); + return getCustomPlanFeaturePermission(billingPlan, "accessControl"); }; export const getOrganizationProjectsLimit = async (