diff --git a/resources/js/admin/mobile/BrandingPage.tsx b/resources/js/admin/mobile/BrandingPage.tsx
index 3e70d5e..50dba44 100644
--- a/resources/js/admin/mobile/BrandingPage.tsx
+++ b/resources/js/admin/mobile/BrandingPage.tsx
@@ -640,340 +640,344 @@ export default function MobileBrandingPage() {
-
-
- {t('events.branding.mode', 'Theme')}
-
-
- setForm((prev) => ({ ...prev, mode: 'light' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, mode: 'auto' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, mode: 'dark' }))}
- disabled={brandingDisabled}
- />
-
-
+ {form.useDefaultBranding ? null : (
+ <>
+
+
+ {t('events.branding.mode', 'Theme')}
+
+
+ setForm((prev) => ({ ...prev, mode: 'light' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, mode: 'auto' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, mode: 'dark' }))}
+ disabled={brandingDisabled}
+ />
+
+
-
-
- {t('events.branding.colors', 'Colors')}
-
- setForm((prev) => ({ ...prev, primary: value }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, accent: value }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, background: value }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, surface: value }))}
- disabled={brandingDisabled}
- />
-
-
-
-
- {t('events.branding.fonts', 'Fonts')}
-
- setForm((prev) => ({ ...prev, headingFont: value }))}
- onPicker={() => {
- setFontField('heading');
- setShowFontsSheet(true);
- }}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, bodyFont: value }))}
- onPicker={() => {
- setFontField('body');
- setShowFontsSheet(true);
- }}
- disabled={brandingDisabled}
- />
-
- {t('events.branding.fontSize', 'Font Size')}
-
-
- setForm((prev) => ({ ...prev, fontSize: 's' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, fontSize: 'm' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, fontSize: 'l' }))}
- disabled={brandingDisabled}
- />
-
-
-
-
-
- {t('events.branding.logo', 'Logo')}
-
-
- {t('events.branding.logoHint', 'Upload a logo or use an emoji for the guest header.')}
-
-
- setForm((prev) => ({ ...prev, logoMode: 'upload' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, logoMode: 'emoticon' }))}
- disabled={brandingDisabled}
- />
-
-
- {form.logoMode === 'emoticon' ? (
- setForm((prev) => ({ ...prev, logoValue: value }))}
- disabled={brandingDisabled}
- />
- ) : (
-
- {form.logoDataUrl ? (
- <>
-
-
- document.getElementById('branding-logo-input')?.click()}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, logoDataUrl: '' }))}
- >
-
-
-
- {t('events.branding.removeLogo', 'Remove')}
-
-
-
-
- >
- ) : (
- <>
-
- document.getElementById('branding-logo-input')?.click()}
- >
-
-
-
- {t('events.branding.uploadLogo', 'Upload logo (max. 1 MB)')}
-
-
-
- >
- )}
-
+
+ {t('events.branding.colors', 'Colors')}
+
+ setForm((prev) => ({ ...prev, primary: value }))}
disabled={brandingDisabled}
- onChange={(event) => {
- const file = event.target.files?.[0];
- if (!file) return;
- if (file.size > 1024 * 1024) {
- setError(t('events.branding.logoTooLarge', 'Logo must be under 1 MB.'));
- return;
- }
- const reader = new FileReader();
- reader.onload = () => {
- const nextLogo =
- typeof reader.result === 'string'
- ? reader.result
- : typeof reader.result === 'object' && reader.result !== null
- ? String(reader.result)
- : '';
- setForm((prev) => ({ ...prev, logoDataUrl: nextLogo }));
- setError(null);
- };
- reader.readAsDataURL(file);
- }}
/>
-
- )}
+ setForm((prev) => ({ ...prev, accent: value }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, background: value }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, surface: value }))}
+ disabled={brandingDisabled}
+ />
+
-
- {t('events.branding.logoPosition', 'Position')}
-
-
- setForm((prev) => ({ ...prev, logoPosition: 'left' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, logoPosition: 'center' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, logoPosition: 'right' }))}
- disabled={brandingDisabled}
- />
-
-
- {t('events.branding.logoSize', 'Size')}
-
-
- setForm((prev) => ({ ...prev, logoSize: 's' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, logoSize: 'm' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, logoSize: 'l' }))}
- disabled={brandingDisabled}
- />
-
-
+
+
+ {t('events.branding.fonts', 'Fonts')}
+
+ setForm((prev) => ({ ...prev, headingFont: value }))}
+ onPicker={() => {
+ setFontField('heading');
+ setShowFontsSheet(true);
+ }}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, bodyFont: value }))}
+ onPicker={() => {
+ setFontField('body');
+ setShowFontsSheet(true);
+ }}
+ disabled={brandingDisabled}
+ />
+
+ {t('events.branding.fontSize', 'Font Size')}
+
+
+ setForm((prev) => ({ ...prev, fontSize: 's' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, fontSize: 'm' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, fontSize: 'l' }))}
+ disabled={brandingDisabled}
+ />
+
+
-
-
- {t('events.branding.buttons', 'Buttons & Links')}
-
-
- {t('events.branding.buttonsHint', 'Style, radius, and link color for CTA buttons.')}
-
-
- setForm((prev) => ({ ...prev, buttonStyle: 'filled' }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, buttonStyle: 'outline' }))}
- disabled={brandingDisabled}
- />
-
- setForm((prev) => ({ ...prev, buttonRadius: value }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, buttonPrimary: value }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, buttonSecondary: value }))}
- disabled={brandingDisabled}
- />
- setForm((prev) => ({ ...prev, linkColor: value }))}
- disabled={brandingDisabled}
- />
-
+
+
+ {t('events.branding.logo', 'Logo')}
+
+
+ {t('events.branding.logoHint', 'Upload a logo or use an emoji for the guest header.')}
+
+
+ setForm((prev) => ({ ...prev, logoMode: 'upload' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, logoMode: 'emoticon' }))}
+ disabled={brandingDisabled}
+ />
+
+
+ {form.logoMode === 'emoticon' ? (
+ setForm((prev) => ({ ...prev, logoValue: value }))}
+ disabled={brandingDisabled}
+ />
+ ) : (
+
+ {form.logoDataUrl ? (
+ <>
+
+
+ document.getElementById('branding-logo-input')?.click()}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, logoDataUrl: '' }))}
+ >
+
+
+
+ {t('events.branding.removeLogo', 'Remove')}
+
+
+
+
+ >
+ ) : (
+ <>
+
+ document.getElementById('branding-logo-input')?.click()}
+ >
+
+
+
+ {t('events.branding.uploadLogo', 'Upload logo (max. 1 MB)')}
+
+
+
+ >
+ )}
+ {
+ const file = event.target.files?.[0];
+ if (!file) return;
+ if (file.size > 1024 * 1024) {
+ setError(t('events.branding.logoTooLarge', 'Logo must be under 1 MB.'));
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = () => {
+ const nextLogo =
+ typeof reader.result === 'string'
+ ? reader.result
+ : typeof reader.result === 'object' && reader.result !== null
+ ? String(reader.result)
+ : '';
+ setForm((prev) => ({ ...prev, logoDataUrl: nextLogo }));
+ setError(null);
+ };
+ reader.readAsDataURL(file);
+ }}
+ />
+
+ )}
+
+
+ {t('events.branding.logoPosition', 'Position')}
+
+
+ setForm((prev) => ({ ...prev, logoPosition: 'left' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, logoPosition: 'center' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, logoPosition: 'right' }))}
+ disabled={brandingDisabled}
+ />
+
+
+ {t('events.branding.logoSize', 'Size')}
+
+
+ setForm((prev) => ({ ...prev, logoSize: 's' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, logoSize: 'm' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, logoSize: 'l' }))}
+ disabled={brandingDisabled}
+ />
+
+
+
+
+
+ {t('events.branding.buttons', 'Buttons & Links')}
+
+
+ {t('events.branding.buttonsHint', 'Style, radius, and link color for CTA buttons.')}
+
+
+ setForm((prev) => ({ ...prev, buttonStyle: 'filled' }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, buttonStyle: 'outline' }))}
+ disabled={brandingDisabled}
+ />
+
+ setForm((prev) => ({ ...prev, buttonRadius: value }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, buttonPrimary: value }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, buttonSecondary: value }))}
+ disabled={brandingDisabled}
+ />
+ setForm((prev) => ({ ...prev, linkColor: value }))}
+ disabled={brandingDisabled}
+ />
+
+ >
+ )}
>
) : (
renderWatermarkTab()
diff --git a/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx b/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx
new file mode 100644
index 0000000..06fcbfd
--- /dev/null
+++ b/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx
@@ -0,0 +1,166 @@
+import React from 'react';
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+
+const navigateMock = vi.fn();
+const backMock = vi.fn();
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => navigateMock,
+ useParams: () => ({ slug: 'demo-event' }),
+}));
+
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string, fallback?: string) => fallback ?? key,
+ }),
+ initReactI18next: {
+ type: '3rdParty',
+ init: () => undefined,
+ },
+}));
+
+vi.mock('react-hot-toast', () => ({
+ default: {
+ error: vi.fn(),
+ success: vi.fn(),
+ },
+}));
+
+vi.mock('../components/MobileShell', () => ({
+ MobileShell: ({ children }: { children: React.ReactNode }) =>
{children}
,
+ HeaderActionButton: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('../components/Primitives', () => ({
+ MobileCard: ({ children }: { children: React.ReactNode }) => {children}
,
+ CTAButton: ({ label }: { label: string }) => ,
+ SkeletonCard: () => Loading...
,
+}));
+
+vi.mock('../components/Sheet', () => ({
+ MobileSheet: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('../hooks/useBackNavigation', () => ({
+ useBackNavigation: () => backMock,
+}));
+
+vi.mock('../theme', () => ({
+ ADMIN_COLORS: {
+ primary: '#ff5a5f',
+ accent: '#6d28d9',
+ },
+ ADMIN_GRADIENTS: { softCard: 'linear-gradient(180deg, #fff, #f3f4f6)' },
+ useAdminTheme: () => ({
+ textStrong: '#0f172a',
+ muted: '#64748b',
+ subtle: '#94a3b8',
+ border: '#e2e8f0',
+ primary: '#ff5a5f',
+ accentSoft: '#f5f3ff',
+ danger: '#dc2626',
+ surfaceMuted: '#f8fafc',
+ surface: '#ffffff',
+ backdrop: '#0f172a',
+ dangerBg: '#fee2e2',
+ dangerText: '#b91c1c',
+ }),
+}));
+
+vi.mock('@tamagui/stacks', () => ({
+ YStack: ({ children }: { children: React.ReactNode }) => {children}
,
+ XStack: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+vi.mock('@tamagui/text', () => ({
+ SizableText: ({ children }: { children: React.ReactNode }) => {children},
+}));
+
+vi.mock('@tamagui/react-native-web-lite', () => ({
+ Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
+
+ ),
+}));
+
+vi.mock('tamagui', () => ({
+ Slider: Object.assign(
+ ({ children }: { children: React.ReactNode }) => {children}
,
+ {
+ Track: ({ children }: { children: React.ReactNode }) => {children}
,
+ TrackActive: ({ children }: { children?: React.ReactNode }) => {children}
,
+ Thumb: () => ,
+ }
+ ),
+}));
+
+const getEventMock = vi.fn();
+const updateEventMock = vi.fn();
+const getTenantFontsMock = vi.fn();
+const getTenantSettingsMock = vi.fn();
+const trackOnboardingMock = vi.fn();
+
+vi.mock('../../api', () => ({
+ TenantEvent: {},
+ TenantFont: {},
+ WatermarkSettings: {},
+ getEvent: (...args: unknown[]) => getEventMock(...args),
+ updateEvent: (...args: unknown[]) => updateEventMock(...args),
+ getTenantFonts: (...args: unknown[]) => getTenantFontsMock(...args),
+ getTenantSettings: (...args: unknown[]) => getTenantSettingsMock(...args),
+ trackOnboarding: (...args: unknown[]) => trackOnboardingMock(...args),
+}));
+
+import MobileBrandingPage from '../BrandingPage';
+
+const baseEvent = {
+ id: 9,
+ name: 'Demo Event',
+ slug: 'demo-event',
+ event_date: null,
+ event_type_id: 1,
+ event_type: null,
+ status: 'draft' as const,
+ settings: {},
+};
+
+describe('MobileBrandingPage', () => {
+ beforeEach(() => {
+ getTenantFontsMock.mockResolvedValue([]);
+ getTenantSettingsMock.mockResolvedValue({ settings: {} });
+ });
+
+ it('hides custom branding controls when default branding is active', async () => {
+ getEventMock.mockResolvedValueOnce({
+ ...baseEvent,
+ settings: { branding: { use_default_branding: true } },
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Branding Source')).toBeInTheDocument();
+ });
+
+ expect(screen.queryByText('Theme')).toBeNull();
+ expect(screen.queryByText('Colors')).toBeNull();
+ });
+
+ it('shows custom branding controls when event branding is active', async () => {
+ getEventMock.mockResolvedValueOnce({
+ ...baseEvent,
+ settings: { branding: { use_default_branding: false } },
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Branding Source')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Theme')).toBeInTheDocument();
+ expect(screen.getByText('Colors')).toBeInTheDocument();
+ });
+});