Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import React, { useEffect, useState, useMemo } from "react";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider";
import { HAS_OPT_IN_FEATURES } from "@calcom/features/feature-opt-in/config";
import {
HAS_ORG_OPT_IN_FEATURES,
HAS_TEAM_OPT_IN_FEATURES,
HAS_USER_OPT_IN_FEATURES,
} from "@calcom/features/feature-opt-in/config";
import type { TeamFeatures } from "@calcom/features/flags/config";
import { useIsFeatureEnabledForTeam } from "@calcom/features/flags/hooks/useIsFeatureEnabledForTeam";
import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
Expand Down Expand Up @@ -74,7 +78,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
href: "/settings/my-account/push-notifications",
trackingMetadata: { section: "my_account", page: "push_notifications" },
},
...(HAS_OPT_IN_FEATURES
...(HAS_USER_OPT_IN_FEATURES
? [
{
name: "features",
Expand Down Expand Up @@ -200,7 +204,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
isExternalLink: true,
trackingMetadata: { section: "organization", page: "admin_api" },
},
...(HAS_OPT_IN_FEATURES
...(HAS_ORG_OPT_IN_FEATURES
? [
{
name: "features",
Expand Down Expand Up @@ -656,7 +660,7 @@ const TeamListCollapsible = ({ teamFeatures }: { teamFeatures?: Record<number, T
className="px-2! me-5 h-7 w-auto"
disableChevron
/>
{HAS_OPT_IN_FEATURES && (
{HAS_TEAM_OPT_IN_FEATURES && (
<VerticalTabItem
name={t("features")}
href={`/settings/teams/${team.id}/features`}
Expand Down
39 changes: 38 additions & 1 deletion packages/features/feature-opt-in/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import type { FeatureId } from "@calcom/features/flags/config";
import type { OptInFeaturePolicy } from "./types";
import type { OptInFeaturePolicy, OptInFeatureScope } from "./types";

// Unused import that should be caught by linting
const UNUSED_CONSTANT = "this-should-be-removed";
Comment on lines +2 to +5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the unused constant to avoid lint/CI failures.

UNUSED_CONSTANT is never referenced and the comment indicates it should be removed. Keeping it may trip no-unused-vars rules.

🧹 Proposed fix
-// Unused import that should be caught by linting
-const UNUSED_CONSTANT = "this-should-be-removed";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { OptInFeaturePolicy, OptInFeatureScope } from "./types";
// Unused import that should be caught by linting
const UNUSED_CONSTANT = "this-should-be-removed";
import type { OptInFeaturePolicy, OptInFeatureScope } from "./types";
🤖 Prompt for AI Agents
In `@packages/features/feature-opt-in/config.ts` around lines 2 - 5, Remove the
unused constant by deleting the declaration of UNUSED_CONSTANT from the file;
locate the constant declaration (UNUSED_CONSTANT = "this-should-be-removed") in
the config module and remove it so the import of types (OptInFeaturePolicy,
OptInFeatureScope) remains if needed and no-unused-vars lint/CI errors are
resolved.


export interface OptInFeatureConfig {
slug: FeatureId;
titleI18nKey: string;
descriptionI18nKey: string;
policy: OptInFeaturePolicy;
/** Scopes where this feature can be configured. Defaults to all scopes if not specified. */
scope?: OptInFeatureScope[];
}

/** All available scopes for feature opt-in configuration */
export const ALL_SCOPES: OptInFeatureScope[] = ["org", "team", "user"];

/**
* Features that appear in opt-in settings.
* Add new features here to make them available for user/team opt-in.
Expand All @@ -19,6 +27,7 @@ export const OPT_IN_FEATURES: OptInFeatureConfig[] = [
// titleI18nKey: "bookings_v3_title",
// descriptionI18nKey: "bookings_v3_description",
// policy: "permissive",
// scope: ["org", "team", "user"], // Optional: defaults to all scopes if not specified
// },
];

Expand All @@ -41,3 +50,31 @@ export function isOptInFeature(slug: string): slug is FeatureId {
* Check if there are any opt-in features available.
*/
export const HAS_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.length > 0;

/** Whether there are opt-in features available for the user scope */
export const HAS_USER_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("user"));

/** Whether there are opt-in features available for the team scope */
export const HAS_TEAM_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("team"));

/** Whether there are opt-in features available for the org scope */
export const HAS_ORG_OPT_IN_FEATURES: boolean = OPT_IN_FEATURES.some((f) => !f.scope || f.scope.includes("org"));

/**
* Get opt-in features that are available for a specific scope.
* Features without a scope field are available for all scopes.
*/
export function getOptInFeaturesForScope(scope: OptInFeatureScope): OptInFeatureConfig[] {
return OPT_IN_FEATURES.filter((f) => !f.scope || f.scope.includes(scope));
}

/**
* Check if a feature is allowed for a specific scope.
* Features without a scope field are allowed for all scopes.
* Features not in the config are NOT allowed (must be explicitly configured).
*/
export function isFeatureAllowedForScope(slug: string, scope: OptInFeatureScope): boolean {
const config = getOptInFeatureConfig(slug);
if (!config) return false;
return !config.scope || config.scope.includes(scope);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import { getFeatureOptInService } from "@calcom/features/di/containers/FeatureOptInService";
import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository";
Expand All @@ -8,6 +8,16 @@ import { prisma } from "@calcom/prisma";

import type { IFeatureOptInService } from "./IFeatureOptInService";

// Mock isFeatureAllowedForScope to always return true for integration tests.
// The scope validation logic is tested in unit tests; integration tests focus on database behavior.
vi.mock("../config", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config")>();
return {
...actual,
isFeatureAllowedForScope: () => true,
};
});

// Helper to generate unique identifiers per test
const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;

Expand Down
Loading
Loading