166 lines
5.7 KiB
TypeScript
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);
|
|
}
|