Files
calendar/packages/features/feature-opt-in/config.ts
T

166 lines
5.7 KiB
TypeScript

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);
}