diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json
index 98c9d37..5b1af7a 100644
--- a/resources/js/admin/i18n/locales/de/management.json
+++ b/resources/js/admin/i18n/locales/de/management.json
@@ -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",
diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json
index c74f2f4..5d565c3 100644
--- a/resources/js/admin/i18n/locales/en/management.json
+++ b/resources/js/admin/i18n/locales/en/management.json
@@ -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",
diff --git a/resources/js/admin/mobile/BillingPage.tsx b/resources/js/admin/mobile/BillingPage.tsx
index d4b94a5..1f507e1 100644
--- a/resources/js/admin/mobile/BillingPage.tsx
+++ b/resources/js/admin/mobile/BillingPage.tsx
@@ -33,6 +33,7 @@ import {
formatEventUsage,
getPackageFeatureLabel,
getPackageLimitEntries,
+ resolveTenantWatermarkFeatureKey,
} from './lib/packageSummary';
import {
PendingCheckout,
@@ -564,7 +565,7 @@ function PackageCard({
) : null}
{isPartnerPackage && includedTierLabel ? {includedTierLabel} : 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}
{eventUsageText ? (
@@ -632,6 +633,16 @@ function renderFeatureBadge(pkg: TenantPackageSummary, t: any, key: string, labe
return {enabled ? label : `${label} off`};
}
+function renderWatermarkBadge(pkg: TenantPackageSummary, t: any) {
+ const featureKey = resolveTenantWatermarkFeatureKey(pkg);
+ if (!featureKey) {
+ return null;
+ }
+
+ const tone = featureKey === 'watermark_base' ? 'muted' : 'success';
+ return {getPackageFeatureLabel(featureKey, t)};
+}
+
function UsageBar({ metric }: { metric: PackageUsageMetric }) {
const { t } = useTranslation('management');
const { muted, textStrong, border, primary, subtle, warningText, danger } = useAdminTheme();
diff --git a/resources/js/admin/mobile/__tests__/packageShop.test.ts b/resources/js/admin/mobile/__tests__/packageShop.test.ts
index 349def4..9dedb2b 100644
--- a/resources/js/admin/mobile/__tests__/packageShop.test.ts
+++ b/resources/js/admin/mobile/__tests__/packageShop.test.ts
@@ -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']);
+ });
});
diff --git a/resources/js/admin/mobile/lib/packageShop.ts b/resources/js/admin/mobile/lib/packageShop.ts
index fd9b58a..b4bfb3a 100644
--- a/resources/js/admin/mobile/lib/packageShop.ts
+++ b/resources/js/admin/mobile/lib/packageShop.ts
@@ -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 {
- 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();
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;
+}
diff --git a/resources/js/admin/mobile/lib/packageSummary.test.ts b/resources/js/admin/mobile/lib/packageSummary.test.ts
index e5c2525..f2706c0 100644
--- a/resources/js/admin/mobile/lib/packageSummary.test.ts
+++ b/resources/js/admin/mobile/lib/packageSummary.test.ts
@@ -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', () => {
diff --git a/resources/js/admin/mobile/lib/packageSummary.ts b/resources/js/admin/mobile/lib/packageSummary.ts
index 81dac27..1c40f83 100644
--- a/resources/js/admin/mobile/lib/packageSummary.ts
+++ b/resources/js/admin/mobile/lib/packageSummary.ts
@@ -80,6 +80,18 @@ const FEATURE_LABELS: Record = {
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;
diff --git a/resources/js/pages/marketing/Packages.tsx b/resources/js/pages/marketing/Packages.tsx
index 2186b74..a9e547d 100644
--- a/resources/js/pages/marketing/Packages.tsx
+++ b/resources/js/pages/marketing/Packages.tsx
@@ -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[] =>
diff --git a/resources/js/pages/marketing/__tests__/Packages.test.ts b/resources/js/pages/marketing/__tests__/Packages.test.ts
index fc431fa..0b7408c 100644
--- a/resources/js/pages/marketing/__tests__/Packages.test.ts
+++ b/resources/js/pages/marketing/__tests__/Packages.test.ts
@@ -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');
});
});
diff --git a/resources/lang/de/marketing.json b/resources/lang/de/marketing.json
index 6b3706d..7c4b7fd 100644
--- a/resources/lang/de/marketing.json
+++ b/resources/lang/de/marketing.json
@@ -89,13 +89,13 @@
"feature_live_slideshow": "Live-Slideshow",
"feature_analytics": "Analytics",
"feature_watermark": "Wasserzeichen",
- "feature_watermark_base": "Unser Wasserzeichen",
+ "feature_watermark_base": "Fotospiel-Wasserzeichen aktiv",
"feature_watermark_custom": "Eigenes Wasserzeichen",
"feature_branding": "Branding",
"feature_support": "Support",
"feature_basic_uploads": "Basis-Uploads",
"feature_unlimited_sharing": "Unbegrenztes Teilen",
- "feature_no_watermark": "Kein Wasserzeichen",
+ "feature_no_watermark": "Fotospiel-Wasserzeichen entfernen",
"feature_custom_tasks": "Benutzerdefinierte Tasks",
"feature_advanced_analytics": "Erweiterte Analytics",
"feature_priority_support": "Priorisierter Support",
diff --git a/resources/lang/de/marketing.php b/resources/lang/de/marketing.php
index 1a58d07..16446d4 100644
--- a/resources/lang/de/marketing.php
+++ b/resources/lang/de/marketing.php
@@ -36,13 +36,13 @@ return [
'feature_live_slideshow' => 'Live-Slideshow',
'feature_analytics' => 'Analytics',
'feature_watermark' => 'Wasserzeichen',
- 'feature_watermark_base' => 'Unser Wasserzeichen',
+ 'feature_watermark_base' => 'Fotospiel-Wasserzeichen aktiv',
'feature_watermark_custom' => 'Eigenes Wasserzeichen',
'feature_branding' => 'Branding',
'feature_support' => 'Support',
'feature_basic_uploads' => 'Grundlegende Uploads',
'feature_unlimited_sharing' => 'Unbegrenztes Teilen',
- 'feature_no_watermark' => 'Kein Wasserzeichen',
+ 'feature_no_watermark' => 'Fotospiel-Wasserzeichen entfernen',
'feature_custom_tasks' => 'Benutzerdefinierte Tasks',
'feature_advanced_analytics' => 'Erweiterte Analytics',
'feature_priority_support' => 'Priorisierter Support',
diff --git a/resources/lang/en/marketing.json b/resources/lang/en/marketing.json
index bd4b8cb..0a9f548 100644
--- a/resources/lang/en/marketing.json
+++ b/resources/lang/en/marketing.json
@@ -89,13 +89,13 @@
"feature_live_slideshow": "Live Slideshow",
"feature_analytics": "Analytics",
"feature_watermark": "Watermark",
- "feature_watermark_base": "Our watermark",
+ "feature_watermark_base": "Fotospiel watermark applied",
"feature_watermark_custom": "Custom watermark",
"feature_branding": "Branding",
"feature_support": "Support",
"feature_basic_uploads": "Basic Uploads",
"feature_unlimited_sharing": "Unlimited Sharing",
- "feature_no_watermark": "No Watermark",
+ "feature_no_watermark": "Remove Fotospiel watermark",
"feature_custom_tasks": "Custom Tasks",
"feature_advanced_analytics": "Advanced Analytics",
"feature_priority_support": "Priority Support",
diff --git a/resources/lang/en/marketing.php b/resources/lang/en/marketing.php
index bf4e62b..7563a6c 100644
--- a/resources/lang/en/marketing.php
+++ b/resources/lang/en/marketing.php
@@ -36,13 +36,13 @@ return [
'feature_live_slideshow' => 'Live Slideshow',
'feature_analytics' => 'Analytics',
'feature_watermark' => 'Watermark',
- 'feature_watermark_base' => 'Our watermark',
+ 'feature_watermark_base' => 'Fotospiel watermark applied',
'feature_watermark_custom' => 'Custom watermark',
'feature_branding' => 'Branding',
'feature_support' => 'Support',
'feature_basic_uploads' => 'Basic Uploads',
'feature_unlimited_sharing' => 'Unlimited Sharing',
- 'feature_no_watermark' => 'No Watermark',
+ 'feature_no_watermark' => 'Remove Fotospiel watermark',
'feature_custom_tasks' => 'Custom Tasks',
'feature_advanced_analytics' => 'Advanced Analytics',
'feature_priority_support' => 'Priority Support',