/** * Entitlements Hook * * Provides utilities for checking feature availability based on the new billing system. * This replaces the legacy usePlanFeatures hook for new billing-aware features. */ import { useQuery } from '@tanstack/react-query'; import { getEntitlements, Entitlements } from '../api/billing'; export interface UseEntitlementsResult { /** * The raw entitlements map */ entitlements: Entitlements; /** * Whether entitlements are still loading */ isLoading: boolean; /** * Check if a boolean feature is enabled */ hasFeature: (featureCode: string) => boolean; /** * Get the limit value for an integer feature * Returns null if the feature doesn't exist or is not an integer */ getLimit: (featureCode: string) => number | null; /** * Refetch entitlements */ refetch: () => void; } /** * Hook to access entitlements from the billing system. * * Usage: * ```tsx * const { hasFeature, getLimit, isLoading } = useEntitlements(); * * if (hasFeature('can_use_sms_reminders')) { * // Show SMS feature * } * * const maxUsers = getLimit('max_users'); * if (maxUsers !== null && currentUsers >= maxUsers) { * // Show upgrade prompt * } * ``` */ export const useEntitlements = (): UseEntitlementsResult => { const { data, isLoading, refetch } = useQuery({ queryKey: ['entitlements'], queryFn: getEntitlements, staleTime: 5 * 60 * 1000, // 5 minutes retry: 1, }); const entitlements = data ?? {}; /** * Check if a boolean feature is enabled. */ const hasFeature = (featureCode: string): boolean => { const value = entitlements[featureCode]; return value === true; }; /** * Get the limit value for an integer feature. * Returns null if the feature doesn't exist or is a boolean. */ const getLimit = (featureCode: string): number | null => { const value = entitlements[featureCode]; // Use strict type check to distinguish integers from booleans // (typeof true === 'number' is false, but just to be safe) if (typeof value === 'number' && !Number.isNaN(value)) { return value; } return null; }; return { entitlements, isLoading, hasFeature, getLimit, refetch: () => refetch(), }; }; /** * Feature code constants for type safety */ export const FEATURE_CODES = { // Boolean features (permissions) CAN_ACCEPT_PAYMENTS: 'can_accept_payments', CAN_USE_CUSTOM_DOMAIN: 'can_use_custom_domain', CAN_WHITE_LABEL: 'can_white_label', CAN_API_ACCESS: 'can_api_access', CAN_USE_SMS_REMINDERS: 'can_use_sms_reminders', CAN_USE_MASKED_PHONE_NUMBERS: 'can_use_masked_phone_numbers', CAN_USE_MOBILE_APP: 'can_use_mobile_app', CAN_USE_CONTRACTS: 'can_use_contracts', CAN_USE_CALENDAR_SYNC: 'can_use_calendar_sync', CAN_USE_WEBHOOKS: 'can_use_webhooks', CAN_USE_PLUGINS: 'can_use_plugins', CAN_USE_TASKS: 'can_use_tasks', CAN_CREATE_PLUGINS: 'can_create_plugins', CAN_EXPORT_DATA: 'can_export_data', CAN_ADD_VIDEO_CONFERENCING: 'can_add_video_conferencing', CAN_BOOK_REPEATED_EVENTS: 'can_book_repeated_events', CAN_REQUIRE_2FA: 'can_require_2fa', CAN_DOWNLOAD_LOGS: 'can_download_logs', CAN_DELETE_DATA: 'can_delete_data', CAN_USE_POS: 'can_use_pos', CAN_MANAGE_OAUTH_CREDENTIALS: 'can_manage_oauth_credentials', CAN_CONNECT_TO_API: 'can_connect_to_api', // Integer features (limits) MAX_USERS: 'max_users', MAX_RESOURCES: 'max_resources', MAX_EVENT_TYPES: 'max_event_types', MAX_CALENDARS_CONNECTED: 'max_calendars_connected', MAX_PUBLIC_PAGES: 'max_public_pages', } as const; export type FeatureCode = (typeof FEATURE_CODES)[keyof typeof FEATURE_CODES];