Consolidate white_label to remove_branding and add embed widget

- Rename white_label feature to remove_branding across frontend/backend
- Update billing catalog, plan features, and permission checks
- Add dark mode support to Recharts tooltips with useDarkMode hook
- Create embeddable booking widget with EmbedBooking page
- Add EmbedWidgetSettings for generating embed code
- Fix Appearance settings page permission check
- Update test files for new feature naming
- Add notes field to User model

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-16 21:20:17 -05:00
parent 73d2bee01a
commit 6a6ad63e7b
31 changed files with 2115 additions and 181 deletions

View File

@@ -23,6 +23,7 @@ import {
Calendar,
Clock,
Users,
Code2,
} from 'lucide-react';
import {
SettingsSidebarSection,
@@ -40,7 +41,7 @@ interface ParentContext {
// Map settings pages to their required plan features
const SETTINGS_PAGE_FEATURES: Record<string, FeatureKey> = {
'/dashboard/settings/branding': 'white_label',
'/dashboard/settings/branding': 'remove_branding',
'/dashboard/settings/custom-domains': 'custom_domain',
'/dashboard/settings/api': 'api_access',
'/dashboard/settings/authentication': 'custom_oauth',
@@ -125,7 +126,7 @@ const SettingsLayout: React.FC = () => {
icon={Palette}
label={t('settings.appearance.title', 'Appearance')}
description={t('settings.appearance.description', 'Logo, colors, theme')}
locked={isLocked('white_label')}
locked={isLocked('remove_branding')}
/>
<SettingsSidebarItem
to="/dashboard/settings/email-templates"
@@ -140,6 +141,12 @@ const SettingsLayout: React.FC = () => {
description={t('settings.customDomains.description', 'Use your own domain')}
locked={isLocked('custom_domain')}
/>
<SettingsSidebarItem
to="/dashboard/settings/embed-widget"
icon={Code2}
label={t('settings.embedWidget.title', 'Embed Widget')}
description={t('settings.embedWidget.sidebarDescription', 'Add booking to your site')}
/>
</SettingsSidebarSection>
{/* Integrations Section */}

View File

@@ -372,7 +372,7 @@ describe('SettingsLayout', () => {
// Reset mock for locked feature tests
mockCanUse.mockImplementation((feature: string) => {
// Lock specific features
if (feature === 'white_label') return false;
if (feature === 'remove_branding') return false;
if (feature === 'custom_domain') return false;
if (feature === 'api_access') return false;
if (feature === 'custom_oauth') return false;
@@ -381,7 +381,7 @@ describe('SettingsLayout', () => {
});
});
it('shows lock icon for Appearance link when white_label is locked', () => {
it('shows lock icon for Appearance link when remove_branding is locked', () => {
renderWithRouter();
const appearanceLink = screen.getByRole('link', { name: /Appearance/i });
const lockIcons = within(appearanceLink).queryAllByTestId('lock-icon');
@@ -461,7 +461,7 @@ describe('SettingsLayout', () => {
it('passes isFeatureLocked to child routes when feature is locked', () => {
mockCanUse.mockImplementation((feature: string) => {
return feature !== 'white_label';
return feature !== 'remove_branding';
});
const ChildComponent = () => {
@@ -485,7 +485,7 @@ describe('SettingsLayout', () => {
);
expect(screen.getByTestId('is-locked')).toHaveTextContent('true');
expect(screen.getByTestId('locked-feature')).toHaveTextContent('white_label');
expect(screen.getByTestId('locked-feature')).toHaveTextContent('remove_branding');
});
it('passes isFeatureLocked as false when feature is unlocked', () => {