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",
"advanced_analytics": "Erweiterte Statistiken",
"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.",
"continue": "Weiter zum Event-Setup",
@@ -2910,7 +2913,10 @@
"live_slideshow": "Live-Slideshow",
"priority_support": "Priorisierter Support",
"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": {
"active": "Aktives Paket",

View File

@@ -2319,7 +2319,10 @@
"event_checklist": "Event checklist",
"advanced_analytics": "Advanced analytics",
"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.",
"continue": "Continue to event setup",
@@ -2912,7 +2915,10 @@
"live_slideshow": "Live slideshow",
"priority_support": "Priority support",
"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": {
"active": "Active Plan",

View File

@@ -33,6 +33,7 @@ import {
formatEventUsage,
getPackageFeatureLabel,
getPackageLimitEntries,
resolveTenantWatermarkFeatureKey,
} from './lib/packageSummary';
import {
PendingCheckout,
@@ -564,7 +565,7 @@ function PackageCard({
) : 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, 'watermark_allowed', t('billing.features.watermark', 'Watermark')) : null}
{!isPartnerPackage ? renderWatermarkBadge(pkg, t) : null}
</XStack>
{eventUsageText ? (
<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>;
}
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 }) {
const { t } = useTranslation('management');
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', () => {
const watermarkPackages = [
{ 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: {} },
] 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, 'no_watermark', active)).toBe(2);
expect(selectRecommendedPackageId(watermarkPackages, 'watermark_custom', active)).toBe(3);
});
});
describe('buildPackageComparisonRows', () => {
it('includes limit rows and enabled feature rows', () => {
const rows = buildPackageComparisonRows([
{ features: { advanced_analytics: true, custom_branding: false } },
{ features: { custom_branding: true, watermark_removal: true } },
{ features: { advanced_analytics: true, custom_branding: false }, watermark_allowed: false },
{ features: { custom_branding: true, no_watermark: true }, watermark_allowed: true },
] as any);
expect(rows.map((row) => row.id)).toEqual([
@@ -81,7 +83,8 @@ describe('buildPackageComparisonRows', () => {
'limit.gallery_days',
'feature.advanced_analytics',
'feature.custom_branding',
'feature.watermark_removal',
'feature.no_watermark',
'feature.watermark_base',
]);
});
});
@@ -90,4 +93,13 @@ describe('getEnabledPackageFeatures', () => {
it('accepts array payloads', () => {
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[] {
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> {
return new Set(normalizePackageFeatures(pkg));
if (!pkg) {
return new Set();
}
return new Set(getEnabledPackageFeatures(pkg));
}
function compareLimit(candidate: number | null, active: number | null): number {
@@ -119,9 +134,12 @@ export function selectRecommendedPackageId(
return null;
}
const candidates = feature === 'watermark_allowed'
? packages.filter((pkg) => pkg.watermark_allowed === true)
: packages.filter((pkg) => normalizePackageFeatures(pkg).includes(feature));
const candidates =
feature === 'watermark_allowed'
? 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) {
return null;
}
@@ -151,7 +169,7 @@ export function buildPackageComparisonRows(packages: Package[]): PackageComparis
const featureKeys = new Set<string>();
packages.forEach((pkg) => {
normalizePackageFeatures(pkg).forEach((key) => {
getEnabledPackageFeatures(pkg).forEach((key) => {
if (key !== 'photos') {
featureKeys.add(key);
}
@@ -168,3 +186,23 @@ export function buildPackageComparisonRows(packages: Package[]): PackageComparis
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,
} 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', () => {

View File

@@ -80,6 +80,18 @@ const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
key: 'mobileDashboard.packageSummary.feature.watermark_allowed',
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 }> = [
@@ -241,13 +253,38 @@ export function collectPackageFeatures(pkg: TenantPackageSummary): string[] {
features.add('branding_allowed');
}
if (pkg.package_type !== 'reseller' && pkg.watermark_allowed) {
features.add('watermark_allowed');
const watermarkFeature = resolveTenantWatermarkFeatureKey(pkg);
if (watermarkFeature) {
['watermark_allowed', 'watermark', 'no_watermark', 'watermark_base', 'watermark_custom'].forEach((key) =>
features.delete(key)
);
features.add(watermarkFeature);
}
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 {
if (limit === null || used === null) {
return null;

View File

@@ -54,7 +54,17 @@ export const resolveWatermarkFeatureKey = (pkg: Package): string => {
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[] =>

View File

@@ -9,7 +9,13 @@ describe('resolveWatermarkFeatureKey', () => {
});
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: false } as any)).toBe('no_watermark');
expect(resolveWatermarkFeatureKey({ slug: 'reseller', watermark_allowed: true } as any)).toBe('watermark_custom');
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');
});
});