Clarify watermark features across packages
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] =>
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user