diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json
index 32e5ca62..d40e2a24 100644
--- a/resources/js/admin/i18n/locales/de/management.json
+++ b/resources/js/admin/i18n/locales/de/management.json
@@ -1924,6 +1924,9 @@
"background": "Hintergrund",
"surface": "Fläche",
"lockedBranding": "Branding ist in diesem Paket gesperrt.",
+ "lockedTitle": "Branding freischalten",
+ "lockedBody": "Upgrade auf Standard oder Premium, um Event-Branding zu nutzen.",
+ "upgradeAction": "Paket upgraden",
"source": "Branding-Quelle",
"sourceHint": "Nutze das Standard-Branding oder passe nur dieses Event an.",
"useDefault": "Standard",
@@ -2003,6 +2006,9 @@
"offsetY": "Y-Achse",
"lockedBranding": "Eigenes Wasserzeichen ist in diesem Paket gesperrt. Standard wird genutzt.",
"lockedDisabled": "Wasserzeichen sind in diesem Paket deaktiviert.",
+ "lockedTitle": "Wasserzeichen freischalten",
+ "lockedBody": "Eigenes Wasserzeichen ist nur im Premium-Paket verfügbar.",
+ "upgradeAction": "Upgrade auf Premium",
"errors": {
"noAsset": "Bitte zuerst ein Wasserzeichen hochladen.",
"fileTooLarge": "Wasserzeichen muss kleiner als 3 MB sein."
diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json
index 14ba498e..de3f8ca5 100644
--- a/resources/js/admin/i18n/locales/en/management.json
+++ b/resources/js/admin/i18n/locales/en/management.json
@@ -1928,6 +1928,9 @@
"background": "Background",
"surface": "Surface",
"lockedBranding": "Branding is locked for this package.",
+ "lockedTitle": "Unlock branding",
+ "lockedBody": "Upgrade to Standard or Premium to unlock event branding.",
+ "upgradeAction": "Upgrade package",
"source": "Branding source",
"sourceHint": "Use the default branding or customize this event only.",
"useDefault": "Default",
@@ -2007,6 +2010,9 @@
"offsetY": "Y-axis",
"lockedBranding": "Custom watermark locked by this package. Using base watermark.",
"lockedDisabled": "Watermarks are disabled for this package.",
+ "lockedTitle": "Unlock watermarks",
+ "lockedBody": "Custom watermarks are available with the Premium package.",
+ "upgradeAction": "Upgrade to Premium",
"errors": {
"noAsset": "Please upload a watermark image first.",
"fileTooLarge": "Watermark must be under 3 MB."
diff --git a/resources/js/admin/mobile/BrandingPage.tsx b/resources/js/admin/mobile/BrandingPage.tsx
index 50dba445..d2dba71c 100644
--- a/resources/js/admin/mobile/BrandingPage.tsx
+++ b/resources/js/admin/mobile/BrandingPage.tsx
@@ -339,10 +339,11 @@ export default function MobileBrandingPage() {
{disabled ? (
- }
- text={t('events.watermark.lockedDisabled', 'Kein Wasserzeichen in diesem Paket.')}
- tone="danger"
+ navigate(adminPath('/mobile/billing/shop?feature=watermark_allowed'))}
/>
) : null}
@@ -605,10 +606,11 @@ export default function MobileBrandingPage() {
{!brandingAllowed ? (
- }
- text={t('events.branding.lockedBranding', 'Branding ist in diesem Paket gesperrt.')}
- tone="danger"
+ navigate(adminPath('/mobile/billing/shop?feature=custom_branding'))}
/>
) : null}
@@ -1468,6 +1470,52 @@ function InfoBadge({ icon, text, tone = 'info' }: { icon?: React.ReactNode; text
);
}
+function UpgradeCard({
+ title,
+ body,
+ actionLabel,
+ onPress,
+}: {
+ title: string;
+ body: string;
+ actionLabel: string;
+ onPress: () => void;
+}) {
+ const { textStrong, muted, border, surface, primary, accentSoft } = useAdminTheme();
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {body}
+
+
+
+
+ );
+}
+
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
const { backdrop, surfaceMuted, border, surface } = useAdminTheme();
return (
diff --git a/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx b/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx
index 06fcbfd1..12ad6cc1 100644
--- a/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx
+++ b/resources/js/admin/mobile/__tests__/BrandingPage.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
-import { render, screen, waitFor } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
const navigateMock = vi.fn();
const backMock = vi.fn();
@@ -163,4 +163,38 @@ describe('MobileBrandingPage', () => {
expect(screen.getByText('Theme')).toBeInTheDocument();
expect(screen.getByText('Colors')).toBeInTheDocument();
});
+
+ it('offers an upgrade CTA when branding is locked', async () => {
+ getEventMock.mockResolvedValueOnce({
+ ...baseEvent,
+ package: { branding_allowed: false, watermark_allowed: false },
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Unlock branding')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Upgrade package')).toBeInTheDocument();
+ });
+
+ it('offers an upgrade CTA on the watermark tab when disabled', async () => {
+ getEventMock.mockResolvedValueOnce({
+ ...baseEvent,
+ package: { branding_allowed: true, watermark_allowed: false },
+ });
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Branding Source')).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByText('Wasserzeichen'));
+
+ await waitFor(() => {
+ expect(screen.getByText('Upgrade to Premium')).toBeInTheDocument();
+ });
+ });
});
diff --git a/resources/js/admin/mobile/__tests__/packageShop.test.ts b/resources/js/admin/mobile/__tests__/packageShop.test.ts
index 2f5e7a47..349def49 100644
--- a/resources/js/admin/mobile/__tests__/packageShop.test.ts
+++ b/resources/js/admin/mobile/__tests__/packageShop.test.ts
@@ -56,6 +56,16 @@ describe('selectRecommendedPackageId', () => {
const active = { id: 10, price: 250, max_photos: 999, max_guests: 999, gallery_days: 365, features: { advanced_analytics: true } } as any;
expect(selectRecommendedPackageId(packages, 'advanced_analytics', active)).toBe(2);
});
+
+ 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: 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);
+ });
});
describe('buildPackageComparisonRows', () => {
diff --git a/resources/js/admin/mobile/lib/packageShop.ts b/resources/js/admin/mobile/lib/packageShop.ts
index b07ec7d2..cc956d64 100644
--- a/resources/js/admin/mobile/lib/packageShop.ts
+++ b/resources/js/admin/mobile/lib/packageShop.ts
@@ -106,7 +106,9 @@ export function selectRecommendedPackageId(
return null;
}
- const candidates = packages.filter((pkg) => normalizePackageFeatures(pkg).includes(feature));
+ const candidates = feature === 'watermark_allowed'
+ ? packages.filter((pkg) => pkg.watermark_allowed === true)
+ : packages.filter((pkg) => normalizePackageFeatures(pkg).includes(feature));
if (candidates.length === 0) {
return null;
}