- 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>
134 lines
3.6 KiB
TypeScript
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];
|