Clarify watermark features across packages
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-20 13:10:49 +01:00
parent cb5d5a2870
commit f88aa40315
13 changed files with 157 additions and 29 deletions

View File

@@ -2317,7 +2317,10 @@
"event_checklist": "Event-Checkliste", "event_checklist": "Event-Checkliste",
"advanced_analytics": "Erweiterte Statistiken", "advanced_analytics": "Erweiterte Statistiken",
"branding_allowed": "Branding", "branding_allowed": "Branding",
"watermark_allowed": "Wasserzeichen" "watermark_allowed": "Wasserzeichen",
"watermark_base": "Fotospiel-Wasserzeichen",
"no_watermark": "Fotospiel-Wasserzeichen entfernen",
"watermark_custom": "Eigenes Wasserzeichen"
}, },
"hint": "Im Billing kannst du dein Paket jederzeit prüfen oder upgraden.", "hint": "Im Billing kannst du dein Paket jederzeit prüfen oder upgraden.",
"continue": "Weiter zum Event-Setup", "continue": "Weiter zum Event-Setup",
@@ -2910,7 +2913,10 @@
"live_slideshow": "Live-Slideshow", "live_slideshow": "Live-Slideshow",
"priority_support": "Priorisierter Support", "priority_support": "Priorisierter Support",
"unlimited_sharing": "Unbegrenztes Teilen", "unlimited_sharing": "Unbegrenztes Teilen",
"watermark_removal": "Kein Wasserzeichen" "watermark_removal": "Kein Wasserzeichen",
"watermark_base": "Fotospiel-Wasserzeichen",
"no_watermark": "Fotospiel-Wasserzeichen entfernen",
"watermark_custom": "Eigenes Wasserzeichen"
}, },
"status": { "status": {
"active": "Aktives Paket", "active": "Aktives Paket",

View File

@@ -2319,7 +2319,10 @@
"event_checklist": "Event checklist", "event_checklist": "Event checklist",
"advanced_analytics": "Advanced analytics", "advanced_analytics": "Advanced analytics",
"branding_allowed": "Branding", "branding_allowed": "Branding",
"watermark_allowed": "Watermarks" "watermark_allowed": "Watermarks",
"watermark_base": "Fotospiel watermark",
"no_watermark": "Remove Fotospiel watermark",
"watermark_custom": "Custom watermark"
}, },
"hint": "You can revisit billing any time to review or upgrade your package.", "hint": "You can revisit billing any time to review or upgrade your package.",
"continue": "Continue to event setup", "continue": "Continue to event setup",
@@ -2912,7 +2915,10 @@
"live_slideshow": "Live slideshow", "live_slideshow": "Live slideshow",
"priority_support": "Priority support", "priority_support": "Priority support",
"unlimited_sharing": "Unlimited sharing", "unlimited_sharing": "Unlimited sharing",
"watermark_removal": "No Watermark" "watermark_removal": "No Watermark",
"watermark_base": "Fotospiel watermark",
"no_watermark": "Remove Fotospiel watermark",
"watermark_custom": "Custom watermark"
}, },
"status": { "status": {
"active": "Active Plan", "active": "Active Plan",

View File

@@ -33,6 +33,7 @@ import {
formatEventUsage, formatEventUsage,
getPackageFeatureLabel, getPackageFeatureLabel,
getPackageLimitEntries, getPackageLimitEntries,
resolveTenantWatermarkFeatureKey,
} from './lib/packageSummary'; } from './lib/packageSummary';
import { import {
PendingCheckout, PendingCheckout,
@@ -564,7 +565,7 @@ function PackageCard({
) : null} ) : null}
{isPartnerPackage && includedTierLabel ? <PillBadge tone="muted">{includedTierLabel}</PillBadge> : null} {isPartnerPackage && includedTierLabel ? <PillBadge tone="muted">{includedTierLabel}</PillBadge> : null}
{!isPartnerPackage ? renderFeatureBadge(pkg, t, 'branding_allowed', t('billing.features.branding', 'Branding')) : null} {!isPartnerPackage ? renderFeatureBadge(pkg, t, 'branding_allowed', t('billing.features.branding', 'Branding')) : null}
{!isPartnerPackage ? renderFeatureBadge(pkg, t, 'watermark_allowed', t('billing.features.watermark', 'Watermark')) : null} {!isPartnerPackage ? renderWatermarkBadge(pkg, t) : null}
</XStack> </XStack>
{eventUsageText ? ( {eventUsageText ? (
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
@@ -632,6 +633,16 @@ function renderFeatureBadge(pkg: TenantPackageSummary, t: any, key: string, labe
return <PillBadge tone={enabled ? 'success' : 'muted'}>{enabled ? label : `${label} off`}</PillBadge>; return <PillBadge tone={enabled ? 'success' : 'muted'}>{enabled ? label : `${label} off`}</PillBadge>;
} }
function renderWatermarkBadge(pkg: TenantPackageSummary, t: any) {
const featureKey = resolveTenantWatermarkFeatureKey(pkg);
if (!featureKey) {
return null;
}
const tone = featureKey === 'watermark_base' ? 'muted' : 'success';
return <PillBadge tone={tone}>{getPackageFeatureLabel(featureKey, t)}</PillBadge>;
}
function UsageBar({ metric }: { metric: PackageUsageMetric }) { function UsageBar({ metric }: { metric: PackageUsageMetric }) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { muted, textStrong, border, primary, subtle, warningText, danger } = useAdminTheme(); const { muted, textStrong, border, primary, subtle, warningText, danger } = useAdminTheme();

View File

@@ -60,19 +60,21 @@ describe('selectRecommendedPackageId', () => {
it('selects the cheapest package with watermark access when requested', () => { it('selects the cheapest package with watermark access when requested', () => {
const watermarkPackages = [ const watermarkPackages = [
{ id: 1, price: 100, watermark_allowed: false, features: {} }, { id: 1, price: 100, watermark_allowed: false, features: {} },
{ id: 2, price: 120, watermark_allowed: true, features: {} }, { id: 2, price: 120, watermark_allowed: true, features: { no_watermark: true } },
{ id: 3, price: 180, watermark_allowed: true, features: {} }, { id: 3, price: 180, watermark_allowed: true, features: {} },
] as any; ] as any;
const active = { id: 1, price: 100, watermark_allowed: false, features: {} } as any; const active = { id: 1, price: 100, watermark_allowed: false, features: {} } as any;
expect(selectRecommendedPackageId(watermarkPackages, 'watermark_allowed', active)).toBe(2); expect(selectRecommendedPackageId(watermarkPackages, 'watermark_allowed', active)).toBe(2);
expect(selectRecommendedPackageId(watermarkPackages, 'no_watermark', active)).toBe(2);
expect(selectRecommendedPackageId(watermarkPackages, 'watermark_custom', active)).toBe(3);
}); });
}); });
describe('buildPackageComparisonRows', () => { describe('buildPackageComparisonRows', () => {
it('includes limit rows and enabled feature rows', () => { it('includes limit rows and enabled feature rows', () => {
const rows = buildPackageComparisonRows([ const rows = buildPackageComparisonRows([
{ features: { advanced_analytics: true, custom_branding: false } }, { features: { advanced_analytics: true, custom_branding: false }, watermark_allowed: false },
{ features: { custom_branding: true, watermark_removal: true } }, { features: { custom_branding: true, no_watermark: true }, watermark_allowed: true },
] as any); ] as any);
expect(rows.map((row) => row.id)).toEqual([ expect(rows.map((row) => row.id)).toEqual([
@@ -81,7 +83,8 @@ describe('buildPackageComparisonRows', () => {
'limit.gallery_days', 'limit.gallery_days',
'feature.advanced_analytics', 'feature.advanced_analytics',
'feature.custom_branding', 'feature.custom_branding',
'feature.watermark_removal', 'feature.no_watermark',
'feature.watermark_base',
]); ]);
}); });
}); });
@@ -90,4 +93,13 @@ describe('getEnabledPackageFeatures', () => {
it('accepts array payloads', () => { it('accepts array payloads', () => {
expect(getEnabledPackageFeatures({ features: ['custom_branding', ''] } as any)).toEqual(['custom_branding']); expect(getEnabledPackageFeatures({ features: ['custom_branding', ''] } as any)).toEqual(['custom_branding']);
}); });
it('adds watermark feature for endcustomer packages', () => {
expect(
getEnabledPackageFeatures({ watermark_allowed: false, features: [] } as any)
).toEqual(['watermark_base']);
expect(
getEnabledPackageFeatures({ watermark_allowed: true, features: ['no_watermark'] } as any)
).toEqual(['no_watermark']);
});
}); });

View File

@@ -41,11 +41,26 @@ function normalizePackageFeatures(pkg: Package | null): string[] {
} }
export function getEnabledPackageFeatures(pkg: Package): string[] { export function getEnabledPackageFeatures(pkg: Package): string[] {
return normalizePackageFeatures(pkg); const features = normalizePackageFeatures(pkg);
const watermarkFeature = resolvePackageWatermarkFeatureKey(pkg, features);
if (watermarkFeature) {
const cleaned = features.filter(
(feature) => !['watermark', 'watermark_allowed', 'no_watermark', 'watermark_base', 'watermark_custom'].includes(feature)
);
cleaned.push(watermarkFeature);
return Array.from(new Set(cleaned));
}
return features;
} }
function collectFeatures(pkg: Package | null): Set<string> { function collectFeatures(pkg: Package | null): Set<string> {
return new Set(normalizePackageFeatures(pkg)); if (!pkg) {
return new Set();
}
return new Set(getEnabledPackageFeatures(pkg));
} }
function compareLimit(candidate: number | null, active: number | null): number { function compareLimit(candidate: number | null, active: number | null): number {
@@ -119,9 +134,12 @@ export function selectRecommendedPackageId(
return null; return null;
} }
const candidates = feature === 'watermark_allowed' const candidates =
? packages.filter((pkg) => pkg.watermark_allowed === true) feature === 'watermark_allowed'
: packages.filter((pkg) => normalizePackageFeatures(pkg).includes(feature)); ? packages.filter((pkg) => pkg.watermark_allowed === true)
: feature?.startsWith('watermark')
? packages.filter((pkg) => resolvePackageWatermarkFeatureKey(pkg, normalizePackageFeatures(pkg)) === feature)
: packages.filter((pkg) => normalizePackageFeatures(pkg).includes(feature));
if (candidates.length === 0) { if (candidates.length === 0) {
return null; return null;
} }
@@ -151,7 +169,7 @@ export function buildPackageComparisonRows(packages: Package[]): PackageComparis
const featureKeys = new Set<string>(); const featureKeys = new Set<string>();
packages.forEach((pkg) => { packages.forEach((pkg) => {
normalizePackageFeatures(pkg).forEach((key) => { getEnabledPackageFeatures(pkg).forEach((key) => {
if (key !== 'photos') { if (key !== 'photos') {
featureKeys.add(key); featureKeys.add(key);
} }
@@ -168,3 +186,23 @@ export function buildPackageComparisonRows(packages: Package[]): PackageComparis
return [...limitRows, ...featureRows]; return [...limitRows, ...featureRows];
} }
function resolvePackageWatermarkFeatureKey(pkg: Package, features: string[]): string | null {
if (pkg.type === 'reseller') {
return null;
}
if (pkg.watermark_allowed === false) {
return 'watermark_base';
}
if (features.includes('no_watermark')) {
return 'no_watermark';
}
if (pkg.watermark_allowed === true) {
return 'watermark_custom';
}
return null;
}

View File

@@ -44,7 +44,9 @@ describe('packageSummary helpers', () => {
watermark_allowed: false, watermark_allowed: false,
} as any); } as any);
expect(result).toEqual(expect.arrayContaining(['custom_branding', 'reseller_dashboard', 'branding_allowed'])); expect(result).toEqual(
expect.arrayContaining(['custom_branding', 'reseller_dashboard', 'branding_allowed', 'watermark_base'])
);
}); });
it('returns labeled limit entries', () => { it('returns labeled limit entries', () => {

View File

@@ -80,6 +80,18 @@ const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
key: 'mobileDashboard.packageSummary.feature.watermark_allowed', key: 'mobileDashboard.packageSummary.feature.watermark_allowed',
fallback: 'Watermarks', fallback: 'Watermarks',
}, },
watermark_base: {
key: 'mobileDashboard.packageSummary.feature.watermark_base',
fallback: 'Fotospiel watermark',
},
no_watermark: {
key: 'mobileDashboard.packageSummary.feature.no_watermark',
fallback: 'Remove Fotospiel watermark',
},
watermark_custom: {
key: 'mobileDashboard.packageSummary.feature.watermark_custom',
fallback: 'Custom watermark',
},
}; };
const LIMIT_LABELS: Array<{ key: string; labelKey: string; fallback: string }> = [ const LIMIT_LABELS: Array<{ key: string; labelKey: string; fallback: string }> = [
@@ -241,13 +253,38 @@ export function collectPackageFeatures(pkg: TenantPackageSummary): string[] {
features.add('branding_allowed'); features.add('branding_allowed');
} }
if (pkg.package_type !== 'reseller' && pkg.watermark_allowed) { const watermarkFeature = resolveTenantWatermarkFeatureKey(pkg);
features.add('watermark_allowed'); if (watermarkFeature) {
['watermark_allowed', 'watermark', 'no_watermark', 'watermark_base', 'watermark_custom'].forEach((key) =>
features.delete(key)
);
features.add(watermarkFeature);
} }
return Array.from(features); return Array.from(features);
} }
export function resolveTenantWatermarkFeatureKey(pkg: TenantPackageSummary): string | null {
if (pkg.package_type === 'reseller') {
return null;
}
if (pkg.watermark_allowed === false) {
return 'watermark_base';
}
const features = Array.isArray(pkg.features) ? pkg.features : [];
if (features.includes('no_watermark')) {
return 'no_watermark';
}
if (pkg.watermark_allowed === true) {
return 'watermark_custom';
}
return null;
}
export function formatEventUsage(used: number | null, limit: number | null, t: Translate): string | null { export function formatEventUsage(used: number | null, limit: number | null, t: Translate): string | null {
if (limit === null || used === null) { if (limit === null || used === null) {
return null; return null;

View File

@@ -54,7 +54,17 @@ export const resolveWatermarkFeatureKey = (pkg: Package): string => {
return 'watermark_custom'; return 'watermark_custom';
} }
return pkg.watermark_allowed === false ? 'no_watermark' : 'watermark'; const features = Array.isArray(pkg.features) ? pkg.features : [];
if (pkg.watermark_allowed === false) {
return 'watermark_base';
}
if (features.includes('no_watermark')) {
return 'no_watermark';
}
return pkg.watermark_allowed === true ? 'watermark_custom' : 'watermark';
}; };
const sortPackagesByPrice = (packages: Package[]): Package[] => const sortPackagesByPrice = (packages: Package[]): Package[] =>

View File

@@ -9,7 +9,13 @@ describe('resolveWatermarkFeatureKey', () => {
}); });
it('falls back to watermark_allowed when slug is unknown', () => { it('falls back to watermark_allowed when slug is unknown', () => {
expect(resolveWatermarkFeatureKey({ slug: 'reseller', watermark_allowed: true } as any)).toBe('watermark'); expect(resolveWatermarkFeatureKey({ slug: 'reseller', watermark_allowed: true } as any)).toBe('watermark_custom');
expect(resolveWatermarkFeatureKey({ slug: 'reseller', watermark_allowed: false } as any)).toBe('no_watermark'); expect(resolveWatermarkFeatureKey({ slug: 'reseller', watermark_allowed: false } as any)).toBe('watermark_base');
});
it('prefers explicit no_watermark features for unknown slugs', () => {
expect(
resolveWatermarkFeatureKey({ slug: 'reseller', watermark_allowed: true, features: ['no_watermark'] } as any)
).toBe('no_watermark');
}); });
}); });

View File

@@ -89,13 +89,13 @@
"feature_live_slideshow": "Live-Slideshow", "feature_live_slideshow": "Live-Slideshow",
"feature_analytics": "Analytics", "feature_analytics": "Analytics",
"feature_watermark": "Wasserzeichen", "feature_watermark": "Wasserzeichen",
"feature_watermark_base": "Unser Wasserzeichen", "feature_watermark_base": "Fotospiel-Wasserzeichen aktiv",
"feature_watermark_custom": "Eigenes Wasserzeichen", "feature_watermark_custom": "Eigenes Wasserzeichen",
"feature_branding": "Branding", "feature_branding": "Branding",
"feature_support": "Support", "feature_support": "Support",
"feature_basic_uploads": "Basis-Uploads", "feature_basic_uploads": "Basis-Uploads",
"feature_unlimited_sharing": "Unbegrenztes Teilen", "feature_unlimited_sharing": "Unbegrenztes Teilen",
"feature_no_watermark": "Kein Wasserzeichen", "feature_no_watermark": "Fotospiel-Wasserzeichen entfernen",
"feature_custom_tasks": "Benutzerdefinierte Tasks", "feature_custom_tasks": "Benutzerdefinierte Tasks",
"feature_advanced_analytics": "Erweiterte Analytics", "feature_advanced_analytics": "Erweiterte Analytics",
"feature_priority_support": "Priorisierter Support", "feature_priority_support": "Priorisierter Support",

View File

@@ -36,13 +36,13 @@ return [
'feature_live_slideshow' => 'Live-Slideshow', 'feature_live_slideshow' => 'Live-Slideshow',
'feature_analytics' => 'Analytics', 'feature_analytics' => 'Analytics',
'feature_watermark' => 'Wasserzeichen', 'feature_watermark' => 'Wasserzeichen',
'feature_watermark_base' => 'Unser Wasserzeichen', 'feature_watermark_base' => 'Fotospiel-Wasserzeichen aktiv',
'feature_watermark_custom' => 'Eigenes Wasserzeichen', 'feature_watermark_custom' => 'Eigenes Wasserzeichen',
'feature_branding' => 'Branding', 'feature_branding' => 'Branding',
'feature_support' => 'Support', 'feature_support' => 'Support',
'feature_basic_uploads' => 'Grundlegende Uploads', 'feature_basic_uploads' => 'Grundlegende Uploads',
'feature_unlimited_sharing' => 'Unbegrenztes Teilen', 'feature_unlimited_sharing' => 'Unbegrenztes Teilen',
'feature_no_watermark' => 'Kein Wasserzeichen', 'feature_no_watermark' => 'Fotospiel-Wasserzeichen entfernen',
'feature_custom_tasks' => 'Benutzerdefinierte Tasks', 'feature_custom_tasks' => 'Benutzerdefinierte Tasks',
'feature_advanced_analytics' => 'Erweiterte Analytics', 'feature_advanced_analytics' => 'Erweiterte Analytics',
'feature_priority_support' => 'Priorisierter Support', 'feature_priority_support' => 'Priorisierter Support',

View File

@@ -89,13 +89,13 @@
"feature_live_slideshow": "Live Slideshow", "feature_live_slideshow": "Live Slideshow",
"feature_analytics": "Analytics", "feature_analytics": "Analytics",
"feature_watermark": "Watermark", "feature_watermark": "Watermark",
"feature_watermark_base": "Our watermark", "feature_watermark_base": "Fotospiel watermark applied",
"feature_watermark_custom": "Custom watermark", "feature_watermark_custom": "Custom watermark",
"feature_branding": "Branding", "feature_branding": "Branding",
"feature_support": "Support", "feature_support": "Support",
"feature_basic_uploads": "Basic Uploads", "feature_basic_uploads": "Basic Uploads",
"feature_unlimited_sharing": "Unlimited Sharing", "feature_unlimited_sharing": "Unlimited Sharing",
"feature_no_watermark": "No Watermark", "feature_no_watermark": "Remove Fotospiel watermark",
"feature_custom_tasks": "Custom Tasks", "feature_custom_tasks": "Custom Tasks",
"feature_advanced_analytics": "Advanced Analytics", "feature_advanced_analytics": "Advanced Analytics",
"feature_priority_support": "Priority Support", "feature_priority_support": "Priority Support",

View File

@@ -36,13 +36,13 @@ return [
'feature_live_slideshow' => 'Live Slideshow', 'feature_live_slideshow' => 'Live Slideshow',
'feature_analytics' => 'Analytics', 'feature_analytics' => 'Analytics',
'feature_watermark' => 'Watermark', 'feature_watermark' => 'Watermark',
'feature_watermark_base' => 'Our watermark', 'feature_watermark_base' => 'Fotospiel watermark applied',
'feature_watermark_custom' => 'Custom watermark', 'feature_watermark_custom' => 'Custom watermark',
'feature_branding' => 'Branding', 'feature_branding' => 'Branding',
'feature_support' => 'Support', 'feature_support' => 'Support',
'feature_basic_uploads' => 'Basic Uploads', 'feature_basic_uploads' => 'Basic Uploads',
'feature_unlimited_sharing' => 'Unlimited Sharing', 'feature_unlimited_sharing' => 'Unlimited Sharing',
'feature_no_watermark' => 'No Watermark', 'feature_no_watermark' => 'Remove Fotospiel watermark',
'feature_custom_tasks' => 'Custom Tasks', 'feature_custom_tasks' => 'Custom Tasks',
'feature_advanced_analytics' => 'Advanced Analytics', 'feature_advanced_analytics' => 'Advanced Analytics',
'feature_priority_support' => 'Priority Support', 'feature_priority_support' => 'Priority Support',