import type { FeatureId } from "@calcom/features/flags/config"; import type { OptInFeaturePolicy, OptInFeatureScope } from "./types"; export type OptInFeatureDisplayLocation = "settings" | "banner"; const DEFAULT_DISPLAY_LOCATIONS: OptInFeatureDisplayLocation[] = ["settings"]; export interface OptInFeatureConfig { slug: FeatureId; i18n: { title: string; name: string; description: string; }; bannerImage: { src: string; width: number; height: number; }; policy: OptInFeaturePolicy; /** Scopes where this feature can be configured. Defaults to all scopes if not specified. */ scope?: OptInFeatureScope[]; /** * Where this feature should be displayed for opt-in. * - 'settings': Show in the Settings page * - 'banner': Show as a banner notification * * Defaults to ['settings'] if omitted. * Use ['settings', 'banner'] for both locations. * Use [] if you want the feature defined but not displayed anywhere. */ displayLocations?: OptInFeatureDisplayLocation[]; /** * Formbricks feedback configuration for delayed survey triggering after opt-in. * When configured, a custom feedback dialog will be shown after the specified delay * has passed since the user opted in, allowing time for feature usage before collecting feedback. */ formbricks?: { /** Number of days to wait after opt-in before showing the feedback dialog */ waitAfterDays: number; /** Where to show the feedback dialog: "all" | "desktop" | "mobile". Defaults to "all". */ showOn?: "all" | "desktop" | "mobile"; /** Formbricks survey ID to submit responses to */ surveyId: string; /** Question IDs for the survey fields */ questions: { /** Question ID for the rating field */ ratingQuestionId: string; /** Question ID for the comment field */ commentQuestionId: string; }; /** i18n key for the feedback dialog title (defaults to "feedback_dialog_title") */ titleKey?: string; /** i18n key for the feedback dialog description (defaults to "feedback_dialog_description") */ descriptionKey?: string; }; } /** * Features that appear in opt-in settings. * Add new features here to make them available for user/team opt-in. */ export const OPT_IN_FEATURES: OptInFeatureConfig[] = [ { slug: "bookings-v3", i18n: { title: "bookings_v3_title", name: "bookings_v3_name", description: "bookings_v3_description", }, bannerImage: { src: "/opt_in_banner_bookings_v3.png", width: 548, height: 348, }, policy: "permissive", displayLocations: ["banner", "settings"], scope: ["org", "team", "user"], // Optional: defaults to all scopes if not specified formbricks: { waitAfterDays: 3, showOn: "desktop", surveyId: "cml6ps4f0psk9ad019a2kzedz", questions: { ratingQuestionId: "ajt82ni0hue3x5qkltj9t359", commentQuestionId: "kwjw0g7vqkgd5w9s61dba8de", }, titleKey: "bookings_v3_feedback_title", descriptionKey: "bookings_v3_feedback_description", }, }, ]; /** * Get the configuration for a specific opt-in feature by slug. */ export function getOptInFeatureConfig(slug: string): OptInFeatureConfig | undefined { return OPT_IN_FEATURES.find((f) => f.slug === slug); } /** * Check if a slug is in the opt-in allowlist. * Acts as a type guard, narrowing the slug to FeatureId when true. */ export function isOptInFeature(slug: string): slug is FeatureId { return OPT_IN_FEATURES.some((f) => f.slug === slug); } /** * Check if a feature should be displayed at a specific location. * Defaults to 'settings' if displayLocations is not specified. */ export function shouldDisplayFeatureAt( feature: OptInFeatureConfig, location: OptInFeatureDisplayLocation ): boolean { return (feature.displayLocations ?? DEFAULT_DISPLAY_LOCATIONS).includes(location); } /** * Whether there are opt-in features available for the user scope in settings. * Only counts features that should be displayed in settings (displayLocations includes 'settings' or is omitted). */ export const HAS_USER_OPT_IN_FEATURES: boolean = getOptInFeaturesForScope("user", "settings").length > 0; /** * Whether there are opt-in features available for the team scope in settings. * Only counts features that should be displayed in settings (displayLocations includes 'settings' or is omitted). */ export const HAS_TEAM_OPT_IN_FEATURES: boolean = getOptInFeaturesForScope("team", "settings").length > 0; /** * Whether there are opt-in features available for the org scope in settings. * Only counts features that should be displayed in settings (displayLocations includes 'settings' or is omitted). */ export const HAS_ORG_OPT_IN_FEATURES: boolean = getOptInFeaturesForScope("org", "settings").length > 0; /** * Get opt-in features that are available for a specific scope. * Features without a scope field are available for all scopes. * Optionally filter by display location (e.g., 'settings' or 'banner'). */ export function getOptInFeaturesForScope( scope: OptInFeatureScope, displayLocation?: OptInFeatureDisplayLocation ): OptInFeatureConfig[] { return OPT_IN_FEATURES.filter((f) => { const scopeMatches = !f.scope || f.scope.includes(scope); if (!scopeMatches) return false; if (displayLocation) { return shouldDisplayFeatureAt(f, displayLocation); } return true; }); } /** * 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); }