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',