Files
smoothschedule/frontend/src/hooks/useEntitlements.ts
poduck 41caccd31a Add max_public_pages feature and site builder access control
- Add max_public_pages billing feature (Free=0, Starter=1, Growth=5, Pro=10)
- Gate site builder access based on max_public_pages entitlement
- Auto-create Site with default booking page for new tenants
- Update PageEditor to use useEntitlements hook for permission checks
- Replace hardcoded limits in BusinessEditModal with DynamicFeaturesEditor
- Add force update functionality for superusers in PlanEditorWizard
- Add comprehensive filters to all safe scripting get_* methods
- Update plugin documentation with full filter reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 00:27:15 -05:00

134 lines
3.6 KiB
TypeScript

/**
* 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<Entitlements>({
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];