Add package summary banner
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-06 12:01:12 +01:00
parent a796973861
commit cc89cc667a
5 changed files with 204 additions and 13 deletions

View File

@@ -2074,9 +2074,25 @@
"unlimited": "Unbegrenzt",
"unknown": "Unbekannt",
"featuresTitle": "Enthaltene Features",
"feature": {
"priority_support": "Priority Support",
"custom_domain": "Eigene Domain",
"analytics": "Analytics",
"team_management": "Team-Management",
"moderation_tools": "Moderations-Tools",
"prints": "Print-Uploads",
"photo_likes_enabled": "Foto-Likes",
"event_checklist": "Event-Checkliste",
"advanced_analytics": "Erweiterte Analytics",
"branding_allowed": "Branding",
"watermark_allowed": "Wasserzeichen"
},
"hint": "Im Billing kannst du dein Paket jederzeit prüfen oder upgraden.",
"continue": "Weiter zum Event-Setup",
"dismiss": "Schließen"
"dismiss": "Schließen",
"bannerTitle": "Deine Paketübersicht",
"bannerSubtitle": "{{name}} ist aktiv. Prüfe Limits & Features.",
"bannerCta": "Ansehen"
},
"pickEvent": "Event auswählen",
"status": {

View File

@@ -2078,9 +2078,25 @@
"unlimited": "Unlimited",
"unknown": "Unknown",
"featuresTitle": "Included features",
"feature": {
"priority_support": "Priority support",
"custom_domain": "Custom domain",
"analytics": "Analytics",
"team_management": "Team management",
"moderation_tools": "Moderation tools",
"prints": "Print uploads",
"photo_likes_enabled": "Photo likes",
"event_checklist": "Event checklist",
"advanced_analytics": "Advanced analytics",
"branding_allowed": "Branding",
"watermark_allowed": "Watermarks"
},
"hint": "You can revisit billing any time to review or upgrade your package.",
"continue": "Continue to event setup",
"dismiss": "Close"
"dismiss": "Close",
"bannerTitle": "Your package summary",
"bannerSubtitle": "{{name}} is active. Review limits & features.",
"bannerCta": "View"
},
"pickEvent": "Select an event",
"status": {

View File

@@ -17,6 +17,7 @@ import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
import { useDevicePermissions } from './hooks/useDevicePermissions';
import { useInstallPrompt } from './hooks/useInstallPrompt';
import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour';
import { formatPackageLimit, getPackageFeatureLabel } from './lib/packageSummary';
import { trackOnboarding } from '../api';
import { useAuth } from '../auth/context';
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
@@ -350,6 +351,11 @@ export default function MobileDashboardPage() {
locale={locale}
/>
) : null;
const showPackageSummaryBanner =
Boolean(activePackage && summarySeenPackageId === activePackage.id) &&
!summaryOpen &&
!packagesLoading &&
!packagesError;
React.useEffect(() => {
if (events.length || isLoading || fallbackLoading || fallbackAttempted) {
@@ -387,6 +393,12 @@ export default function MobileDashboardPage() {
if (!effectiveHasEvents) {
return (
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
{showPackageSummaryBanner ? (
<PackageSummaryBanner
packageName={activePackage?.package_name}
onOpen={() => setSummaryOpen(true)}
/>
) : null}
<OnboardingEmptyState
installPrompt={installPrompt}
pushState={pushState}
@@ -406,6 +418,12 @@ export default function MobileDashboardPage() {
title={t('mobileDashboard.title', 'Dashboard')}
subtitle={t('mobileDashboard.selectEvent', 'Select an event to continue')}
>
{showPackageSummaryBanner ? (
<PackageSummaryBanner
packageName={activePackage?.package_name}
onOpen={() => setSummaryOpen(true)}
/>
) : null}
<EventPickerList events={effectiveEvents} locale={locale} text={text} muted={muted} border={border} />
{tourSheet}
{packageSummarySheet}
@@ -419,6 +437,12 @@ export default function MobileDashboardPage() {
title={resolveEventDisplayName(activeEvent ?? undefined)}
subtitle={formatEventDate(activeEvent?.event_date, locale) ?? undefined}
>
{showPackageSummaryBanner ? (
<PackageSummaryBanner
packageName={activePackage?.package_name}
onOpen={() => setSummaryOpen(true)}
/>
) : null}
<DeviceSetupCard
installPrompt={installPrompt}
pushState={pushState}
@@ -486,11 +510,6 @@ function PackageSummarySheet({
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
const hasFeatures = Array.isArray(features) && features.length > 0;
const formatLimit = (value: number | null) =>
value === null || value === undefined
? t('mobileDashboard.packageSummary.unlimited', 'Unlimited')
: String(value);
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
return (
@@ -506,12 +525,12 @@ function PackageSummarySheet({
</Text>
</YStack>
<YStack space="$2" marginTop="$2">
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatLimit(maxPhotos)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatLimit(maxGuests)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatLimit(galleryDays)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitPhotos', 'Photos')} value={formatPackageLimit(maxPhotos, t)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitGuests', 'Guests')} value={formatPackageLimit(maxGuests, t)} />
<SummaryRow label={t('mobileDashboard.packageSummary.limitDays', 'Gallery days')} value={formatPackageLimit(galleryDays, t)} />
<SummaryRow
label={t('mobileDashboard.packageSummary.remaining', 'Remaining events')}
value={remainingEvents === null || remainingEvents === undefined ? t('mobileDashboard.packageSummary.unlimited', 'Unlimited') : String(remainingEvents)}
value={formatPackageLimit(remainingEvents, t)}
/>
<SummaryRow label={t('mobileDashboard.packageSummary.purchased', 'Purchased')} value={formatDate(purchasedAt)} />
{expiresAt ? (
@@ -520,7 +539,7 @@ function PackageSummarySheet({
</YStack>
</MobileCard>
{hasFeatures ? (
{hasFeatures ? (
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
@@ -532,7 +551,7 @@ function PackageSummarySheet({
<Sparkles size={14} color={primary} />
</XStack>
<Text fontSize="$xs" color={text}>
{t(`mobileDashboard.packageSummary.feature.${feature}`, feature)}
{getPackageFeatureLabel(feature, t)}
</Text>
</XStack>
))}
@@ -569,6 +588,54 @@ function SummaryRow({ label, value }: { label: string; value: string }) {
);
}
function PackageSummaryBanner({
packageName,
onOpen,
}: {
packageName?: string | null;
onOpen: () => void;
}) {
const { t } = useTranslation('management');
const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme();
const text = textStrong;
return (
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="center" space="$2" flex={1}>
<XStack
width={36}
height={36}
borderRadius={12}
backgroundColor={accentSoft}
alignItems="center"
justifyContent="center"
>
<Sparkles size={16} color={primary} />
</XStack>
<YStack space="$0.5" flex={1}>
<Text fontSize="$sm" fontWeight="800" color={text}>
{t('mobileDashboard.packageSummary.bannerTitle', 'Your package summary')}
</Text>
<Text fontSize="$xs" color={muted}>
{t('mobileDashboard.packageSummary.bannerSubtitle', {
name: packageName ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary'),
defaultValue: '{{name}} is active. Review limits & features.',
})}
</Text>
</YStack>
</XStack>
<CTAButton
label={t('mobileDashboard.packageSummary.bannerCta', 'View')}
tone="ghost"
fullWidth={false}
onPress={onOpen}
/>
</XStack>
</MobileCard>
);
}
function DeviceSetupCard({ installPrompt, pushState, devicePermissions, onOpenSettings }: DeviceSetupProps) {
const { t } = useTranslation('management');
const { textStrong, muted, border, primary, accentSoft } = useAdminTheme();

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';
import { formatPackageLimit, getPackageFeatureLabel } from './packageSummary';
const t = (key: string, options?: Record<string, unknown> | string) => {
if (typeof options === 'string') {
return options;
}
return (options?.defaultValue as string | undefined) ?? key;
};
describe('packageSummary helpers', () => {
it('returns translated labels for known features', () => {
expect(getPackageFeatureLabel('priority_support', t)).toBe('Priority support');
});
it('falls back to raw feature key for unknown features', () => {
expect(getPackageFeatureLabel('custom_feature', t)).toBe('custom_feature');
});
it('formats unlimited package limits', () => {
expect(formatPackageLimit(null, t)).toBe('Unlimited');
});
it('formats numeric package limits', () => {
expect(formatPackageLimit(12, t)).toBe('12');
});
});

View File

@@ -0,0 +1,65 @@
type Translate = (key: string, options?: Record<string, unknown> | string) => string;
const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
priority_support: {
key: 'mobileDashboard.packageSummary.feature.priority_support',
fallback: 'Priority support',
},
custom_domain: {
key: 'mobileDashboard.packageSummary.feature.custom_domain',
fallback: 'Custom domain',
},
analytics: {
key: 'mobileDashboard.packageSummary.feature.analytics',
fallback: 'Analytics',
},
team_management: {
key: 'mobileDashboard.packageSummary.feature.team_management',
fallback: 'Team management',
},
moderation_tools: {
key: 'mobileDashboard.packageSummary.feature.moderation_tools',
fallback: 'Moderation tools',
},
prints: {
key: 'mobileDashboard.packageSummary.feature.prints',
fallback: 'Print uploads',
},
photo_likes_enabled: {
key: 'mobileDashboard.packageSummary.feature.photo_likes_enabled',
fallback: 'Photo likes',
},
event_checklist: {
key: 'mobileDashboard.packageSummary.feature.event_checklist',
fallback: 'Event checklist',
},
advanced_analytics: {
key: 'mobileDashboard.packageSummary.feature.advanced_analytics',
fallback: 'Advanced analytics',
},
branding_allowed: {
key: 'mobileDashboard.packageSummary.feature.branding_allowed',
fallback: 'Branding',
},
watermark_allowed: {
key: 'mobileDashboard.packageSummary.feature.watermark_allowed',
fallback: 'Watermarks',
},
};
export function getPackageFeatureLabel(feature: string, t: Translate): string {
const entry = FEATURE_LABELS[feature];
if (entry) {
return t(entry.key, entry.fallback);
}
return t(`mobileDashboard.packageSummary.feature.${feature}`, feature);
}
export function formatPackageLimit(value: number | null | undefined, t: Translate): string {
if (value === null || value === undefined) {
return t('mobileDashboard.packageSummary.unlimited', 'Unlimited');
}
return String(value);
}