Adjust watermark permissions and transparency
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-19 13:45:43 +01:00
parent fbff2afa3e
commit d4ab9a3a20
15 changed files with 325 additions and 54 deletions

View File

@@ -103,6 +103,7 @@ export type TenantEvent = {
live_show?: LiveShowSettings;
watermark?: WatermarkSettings;
watermark_allowed?: boolean | null;
watermark_removal_allowed?: boolean | null;
watermark_serve_originals?: boolean | null;
};
package?: {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { isBrandingAllowed, isWatermarkAllowed } from '../events';
import { isBrandingAllowed, isWatermarkAllowed, isWatermarkRemovalAllowed } from '../events';
describe('event branding access helpers', () => {
it('respects package-level disallow', () => {
@@ -25,5 +25,14 @@ describe('event branding access helpers', () => {
it('defaults to allow when nothing is set', () => {
expect(isBrandingAllowed({} as any)).toBe(true);
expect(isWatermarkAllowed({} as any)).toBe(true);
expect(isWatermarkRemovalAllowed({} as any)).toBe(false);
});
it('uses removal flag from settings', () => {
const event = {
settings: { watermark_removal_allowed: true },
};
expect(isWatermarkRemovalAllowed(event as any)).toBe(true);
});
});

View File

@@ -116,6 +116,15 @@ export function isWatermarkAllowed(event?: TenantEvent | null): boolean {
return true;
}
export function isWatermarkRemovalAllowed(event?: TenantEvent | null): boolean {
if (!event) return false;
const settings = (event.settings ?? {}) as Record<string, unknown>;
if (typeof settings.watermark_removal_allowed === 'boolean') {
return settings.watermark_removal_allowed;
}
return false;
}
export function formatEventStatusLabel(
status: TenantEvent['status'] | null,
t: (key: string, options?: Record<string, unknown>) => string,

View File

@@ -12,7 +12,7 @@ import { MobileColorInput, MobileField, MobileFileInput, MobileInput, MobileSele
import { TenantEvent, getEvent, updateEvent, getTenantFonts, getTenantSettings, TenantFont, WatermarkSettings, trackOnboarding } from '../api';
import { isAuthError } from '../auth/tokens';
import { ApiError, getApiErrorMessage } from '../lib/apiError';
import { isBrandingAllowed, isWatermarkAllowed } from '../lib/events';
import { isBrandingAllowed, isWatermarkAllowed, isWatermarkRemovalAllowed } from '../lib/events';
import { MobileSheet } from './components/Sheet';
import toast from 'react-hot-toast';
import { adminPath } from '../constants';
@@ -188,10 +188,24 @@ export default function MobileBrandingPage() {
const previewLogoValue = previewForm.logoMode === 'emoticon' ? previewForm.logoValue : '';
const previewInitials = getInitials(previewTitle);
const watermarkAllowed = isWatermarkAllowed(event ?? null);
const watermarkRemovalAllowed = isWatermarkRemovalAllowed(event ?? null);
const brandingAllowed = isBrandingAllowed(event ?? null);
const customWatermarkAllowed = watermarkAllowed && brandingAllowed;
const watermarkLocked = watermarkAllowed && !brandingAllowed;
const brandingDisabled = !brandingAllowed || form.useDefaultBranding;
React.useEffect(() => {
setWatermarkForm((prev) => {
if (prev.mode === 'custom' && !customWatermarkAllowed) {
return { ...prev, mode: 'base' };
}
if (prev.mode === 'off' && !watermarkRemovalAllowed) {
return { ...prev, mode: 'base' };
}
return prev;
});
}, [customWatermarkAllowed, watermarkRemovalAllowed]);
async function handleSave() {
if (!event?.slug) return;
setSaving(true);
@@ -271,7 +285,12 @@ export default function MobileBrandingPage() {
size: form.logoSize,
},
};
const watermarkPayload = buildWatermarkPayload(watermarkForm, watermarkAllowed, brandingAllowed);
const watermarkPayload = buildWatermarkPayload(
watermarkForm,
watermarkAllowed,
brandingAllowed,
watermarkRemovalAllowed
);
if (watermarkPayload) {
settings.watermark = watermarkPayload;
}
@@ -307,10 +326,14 @@ export default function MobileBrandingPage() {
}
function renderWatermarkTab() {
const policyLabel = watermarkAllowed ? 'basic' : 'none';
const disabled = !watermarkAllowed;
const controlsLocked = watermarkLocked || disabled;
const controlsLocked = watermarkLocked;
const mode = controlsLocked ? 'base' : watermarkForm.mode;
const resolvedMode = mode === 'custom' && !customWatermarkAllowed
? 'base'
: mode === 'off' && !watermarkRemovalAllowed
? 'base'
: mode;
const customizationDisabled = controlsLocked || resolvedMode !== 'custom';
return (
<>
@@ -325,12 +348,12 @@ export default function MobileBrandingPage() {
padding={watermarkForm.padding}
offsetX={watermarkForm.offsetX}
offsetY={watermarkForm.offsetY}
previewUrl={mode === 'off' ? '' : watermarkForm.assetDataUrl || watermarkForm.assetPreviewUrl}
previewUrl={resolvedMode === 'off' ? '' : watermarkForm.assetDataUrl || watermarkForm.assetPreviewUrl}
previewAlt={t('events.watermark.previewAlt', 'Watermark preview')}
/>
</MobileCard>
{disabled ? (
{!watermarkAllowed ? (
<UpgradeCard
title={t('events.watermark.lockedTitle', 'Unlock watermarks')}
body={t('events.watermark.lockedBody', 'Custom watermarks are available with the Premium package.')}
@@ -353,7 +376,7 @@ export default function MobileBrandingPage() {
<MobileField label={t('events.watermark.mode', 'Modus')}>
<MobileSelect
value={mode}
value={resolvedMode}
disabled={controlsLocked}
onChange={(event) => {
const value = event.target.value;
@@ -364,16 +387,16 @@ export default function MobileBrandingPage() {
}}
>
<option value="base">{t('events.watermark.modeBase', 'Basis')}</option>
<option value="custom" disabled={watermarkLocked}>
<option value="custom" disabled={!customWatermarkAllowed}>
{t('events.watermark.modeCustom', 'Eigenes Wasserzeichen')}
</option>
<option value="off" disabled={policyLabel === 'basic'}>
<option value="off" disabled={!watermarkRemovalAllowed}>
{t('events.watermark.modeOff', 'Deaktiviert')}
</option>
</MobileSelect>
</MobileField>
{mode === 'custom' && !controlsLocked ? (
{resolvedMode === 'custom' && !controlsLocked ? (
<YStack space="$2">
<Text fontSize="$sm" fontWeight="700" color={textStrong}>
{t('events.watermark.upload', 'Wasserzeichen hochladen')}
@@ -452,7 +475,7 @@ export default function MobileBrandingPage() {
<PositionGrid
value={watermarkForm.position}
onChange={(next) => setWatermarkForm((prev) => ({ ...prev, position: next }))}
disabled={controlsLocked}
disabled={customizationDisabled}
/>
<LabeledSlider
label={t('events.watermark.size', 'Größe')}
@@ -461,7 +484,7 @@ export default function MobileBrandingPage() {
max={60}
step={1}
onChange={(value) => setWatermarkForm((prev) => ({ ...prev, scale: value / 100 }))}
disabled={controlsLocked}
disabled={customizationDisabled}
/>
<LabeledSlider
label={t('events.watermark.opacity', 'Transparenz')}
@@ -470,7 +493,7 @@ export default function MobileBrandingPage() {
max={80}
step={5}
onChange={(value) => setWatermarkForm((prev) => ({ ...prev, opacity: value / 100 }))}
disabled={controlsLocked}
disabled={customizationDisabled}
/>
<LabeledSlider
label={t('events.watermark.padding', 'Abstand zum Rand')}
@@ -479,7 +502,7 @@ export default function MobileBrandingPage() {
max={80}
step={2}
onChange={(value) => setWatermarkForm((prev) => ({ ...prev, padding: value }))}
disabled={controlsLocked}
disabled={customizationDisabled}
/>
<LabeledSlider
label={t('events.watermark.offset', 'Feinjustierung')}
@@ -488,7 +511,7 @@ export default function MobileBrandingPage() {
max={30}
step={1}
onChange={(value) => setWatermarkForm((prev) => ({ ...prev, offsetX: value }))}
disabled={controlsLocked}
disabled={customizationDisabled}
suffix={t('events.watermark.offsetX', 'X-Achse')}
/>
<LabeledSlider
@@ -498,7 +521,7 @@ export default function MobileBrandingPage() {
max={30}
step={1}
onChange={(value) => setWatermarkForm((prev) => ({ ...prev, offsetY: value }))}
disabled={controlsLocked}
disabled={customizationDisabled}
/>
</MobileCard>
</>
@@ -1095,15 +1118,19 @@ function extractWatermark(event: TenantEvent): WatermarkForm {
function buildWatermarkPayload(
form: WatermarkForm,
watermarkAllowed: boolean,
brandingAllowed: boolean
brandingAllowed: boolean,
removalAllowed: boolean
): WatermarkSettings | null {
if (!watermarkAllowed) {
return { mode: 'off' };
const customAllowed = watermarkAllowed && brandingAllowed;
let mode = form.mode;
if (mode === 'custom' && !customAllowed) {
mode = 'base';
}
const policy = watermarkAllowed ? 'basic' : 'none';
const desiredMode = brandingAllowed ? form.mode : 'base';
const mode = desiredMode === 'off' && policy === 'basic' ? 'base' : desiredMode;
if (mode === 'off' && !removalAllowed) {
mode = 'base';
}
const payload: WatermarkSettings = {
mode,
@@ -1115,7 +1142,7 @@ function buildWatermarkPayload(
offset_y: Math.max(-500, Math.min(500, Math.round(form.offsetY))),
};
if (mode === 'custom' && brandingAllowed) {
if (mode === 'custom' && customAllowed) {
if (form.assetDataUrl) {
payload.asset_data_url = form.assetDataUrl;
}