Add upgrade CTAs for branding and watermarks
This commit is contained in:
@@ -1924,6 +1924,9 @@
|
|||||||
"background": "Hintergrund",
|
"background": "Hintergrund",
|
||||||
"surface": "Fläche",
|
"surface": "Fläche",
|
||||||
"lockedBranding": "Branding ist in diesem Paket gesperrt.",
|
"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",
|
"source": "Branding-Quelle",
|
||||||
"sourceHint": "Nutze das Standard-Branding oder passe nur dieses Event an.",
|
"sourceHint": "Nutze das Standard-Branding oder passe nur dieses Event an.",
|
||||||
"useDefault": "Standard",
|
"useDefault": "Standard",
|
||||||
@@ -2003,6 +2006,9 @@
|
|||||||
"offsetY": "Y-Achse",
|
"offsetY": "Y-Achse",
|
||||||
"lockedBranding": "Eigenes Wasserzeichen ist in diesem Paket gesperrt. Standard wird genutzt.",
|
"lockedBranding": "Eigenes Wasserzeichen ist in diesem Paket gesperrt. Standard wird genutzt.",
|
||||||
"lockedDisabled": "Wasserzeichen sind in diesem Paket deaktiviert.",
|
"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": {
|
"errors": {
|
||||||
"noAsset": "Bitte zuerst ein Wasserzeichen hochladen.",
|
"noAsset": "Bitte zuerst ein Wasserzeichen hochladen.",
|
||||||
"fileTooLarge": "Wasserzeichen muss kleiner als 3 MB sein."
|
"fileTooLarge": "Wasserzeichen muss kleiner als 3 MB sein."
|
||||||
|
|||||||
@@ -1928,6 +1928,9 @@
|
|||||||
"background": "Background",
|
"background": "Background",
|
||||||
"surface": "Surface",
|
"surface": "Surface",
|
||||||
"lockedBranding": "Branding is locked for this package.",
|
"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",
|
"source": "Branding source",
|
||||||
"sourceHint": "Use the default branding or customize this event only.",
|
"sourceHint": "Use the default branding or customize this event only.",
|
||||||
"useDefault": "Default",
|
"useDefault": "Default",
|
||||||
@@ -2007,6 +2010,9 @@
|
|||||||
"offsetY": "Y-axis",
|
"offsetY": "Y-axis",
|
||||||
"lockedBranding": "Custom watermark locked by this package. Using base watermark.",
|
"lockedBranding": "Custom watermark locked by this package. Using base watermark.",
|
||||||
"lockedDisabled": "Watermarks are disabled for this package.",
|
"lockedDisabled": "Watermarks are disabled for this package.",
|
||||||
|
"lockedTitle": "Unlock watermarks",
|
||||||
|
"lockedBody": "Custom watermarks are available with the Premium package.",
|
||||||
|
"upgradeAction": "Upgrade to Premium",
|
||||||
"errors": {
|
"errors": {
|
||||||
"noAsset": "Please upload a watermark image first.",
|
"noAsset": "Please upload a watermark image first.",
|
||||||
"fileTooLarge": "Watermark must be under 3 MB."
|
"fileTooLarge": "Watermark must be under 3 MB."
|
||||||
|
|||||||
@@ -339,10 +339,11 @@ export default function MobileBrandingPage() {
|
|||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|
||||||
{disabled ? (
|
{disabled ? (
|
||||||
<InfoBadge
|
<UpgradeCard
|
||||||
icon={<Lock size={16} color={danger} />}
|
title={t('events.watermark.lockedTitle', 'Unlock watermarks')}
|
||||||
text={t('events.watermark.lockedDisabled', 'Kein Wasserzeichen in diesem Paket.')}
|
body={t('events.watermark.lockedBody', 'Custom watermarks are available with the Premium package.')}
|
||||||
tone="danger"
|
actionLabel={t('events.watermark.upgradeAction', 'Upgrade to Premium')}
|
||||||
|
onPress={() => navigate(adminPath('/mobile/billing/shop?feature=watermark_allowed'))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -605,10 +606,11 @@ export default function MobileBrandingPage() {
|
|||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|
||||||
{!brandingAllowed ? (
|
{!brandingAllowed ? (
|
||||||
<InfoBadge
|
<UpgradeCard
|
||||||
icon={<Lock size={16} color={danger} />}
|
title={t('events.branding.lockedTitle', 'Unlock branding')}
|
||||||
text={t('events.branding.lockedBranding', 'Branding ist in diesem Paket gesperrt.')}
|
body={t('events.branding.lockedBody', 'Upgrade to Standard or Premium to unlock event branding.')}
|
||||||
tone="danger"
|
actionLabel={t('events.branding.upgradeAction', 'Upgrade package')}
|
||||||
|
onPress={() => navigate(adminPath('/mobile/billing/shop?feature=custom_branding'))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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 (
|
||||||
|
<MobileCard
|
||||||
|
space="$4"
|
||||||
|
padding="$6"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
borderColor={border}
|
||||||
|
backgroundColor={surface}
|
||||||
|
>
|
||||||
|
<YStack
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
borderRadius={32}
|
||||||
|
backgroundColor={accentSoft}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
marginBottom="$2"
|
||||||
|
>
|
||||||
|
<Lock size={32} color={primary} />
|
||||||
|
</YStack>
|
||||||
|
<YStack space="$2" alignItems="center">
|
||||||
|
<Text fontSize="$xl" fontWeight="900" color={textStrong} textAlign="center">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$sm" color={muted} textAlign="center">
|
||||||
|
{body}
|
||||||
|
</Text>
|
||||||
|
</YStack>
|
||||||
|
<CTAButton label={actionLabel} onPress={onPress} />
|
||||||
|
</MobileCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
|
function TabButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
|
||||||
const { backdrop, surfaceMuted, border, surface } = useAdminTheme();
|
const { backdrop, surfaceMuted, border, surface } = useAdminTheme();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
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 navigateMock = vi.fn();
|
||||||
const backMock = vi.fn();
|
const backMock = vi.fn();
|
||||||
@@ -163,4 +163,38 @@ describe('MobileBrandingPage', () => {
|
|||||||
expect(screen.getByText('Theme')).toBeInTheDocument();
|
expect(screen.getByText('Theme')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Colors')).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(<MobileBrandingPage />);
|
||||||
|
|
||||||
|
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(<MobileBrandingPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Branding Source')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Wasserzeichen'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Upgrade to Premium')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
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);
|
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', () => {
|
describe('buildPackageComparisonRows', () => {
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ export function selectRecommendedPackageId(
|
|||||||
return null;
|
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) {
|
if (candidates.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user