184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
import React from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { Check, Package as PackageIcon } from 'lucide-react';
|
||
import { YStack, XStack } from '@tamagui/stacks';
|
||
import { SizableText as Text } from '@tamagui/text';
|
||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||
import { OnboardingShell } from '../components/OnboardingShell';
|
||
import { MobileCard, CTAButton, PillBadge } from '../components/Primitives';
|
||
import { getPackages, getTenantPackagesOverview, Package, trackOnboarding } from '../../api';
|
||
import { ADMIN_WELCOME_BASE_PATH, ADMIN_WELCOME_EVENT_PATH, ADMIN_WELCOME_SUMMARY_PATH, adminPath } from '../../constants';
|
||
import { getSelectedPackageId, setSelectedPackageId } from '../lib/onboardingSelection';
|
||
import { useAdminTheme } from '../theme';
|
||
|
||
export default function WelcomePackagesPage() {
|
||
const navigate = useNavigate();
|
||
const { t } = useTranslation('onboarding');
|
||
const { muted } = useAdminTheme();
|
||
const [selectedId, setSelectedId] = React.useState<number | null>(() => getSelectedPackageId());
|
||
|
||
const { data: overview } = useQuery({
|
||
queryKey: ['mobile', 'onboarding', 'packages-overview'],
|
||
queryFn: () => getTenantPackagesOverview({ force: true }),
|
||
staleTime: 60_000,
|
||
});
|
||
|
||
const { data: packages, isLoading, isError } = useQuery({
|
||
queryKey: ['mobile', 'onboarding', 'packages-list'],
|
||
queryFn: () => getPackages('endcustomer'),
|
||
staleTime: 60_000,
|
||
});
|
||
|
||
const hasActivePackage =
|
||
Boolean(overview?.activePackage) || Boolean(overview?.packages?.some((pkg) => pkg.active));
|
||
|
||
React.useEffect(() => {
|
||
if (!hasActivePackage) {
|
||
return;
|
||
}
|
||
setSelectedPackageId(null);
|
||
navigate(ADMIN_WELCOME_EVENT_PATH, { replace: true });
|
||
}, [hasActivePackage, navigate]);
|
||
|
||
const handleSelect = (pkg: Package) => {
|
||
setSelectedId(pkg.id);
|
||
setSelectedPackageId(pkg.id);
|
||
void trackOnboarding('package_selected', { package_id: pkg.id, package_name: pkg.name });
|
||
};
|
||
|
||
return (
|
||
<OnboardingShell
|
||
eyebrow={t('packages.layout.eyebrow', 'Step 2')}
|
||
title={t('packages.layout.title', 'Choose your package')}
|
||
subtitle={t(
|
||
'packages.layout.subtitle',
|
||
'Fotospiel supports flexible pricing: single-use event slots or subscriptions covering multiple events.',
|
||
)}
|
||
onBack={() => navigate(ADMIN_WELCOME_BASE_PATH)}
|
||
onSkip={() => navigate(adminPath('/mobile/billing#packages'))}
|
||
skipLabel={t('packages.cta.billing.button', 'Open billing')}
|
||
>
|
||
{isLoading ? (
|
||
<MobileCard>
|
||
<Text fontSize="$sm" color={muted}>
|
||
{t('packages.state.loading', 'Loading packages …')}
|
||
</Text>
|
||
</MobileCard>
|
||
) : isError ? (
|
||
<MobileCard>
|
||
<Text fontSize="$sm" fontWeight="700">
|
||
{t('packages.state.errorTitle', 'Failed to load')}
|
||
</Text>
|
||
<Text fontSize="$sm" color={muted}>
|
||
{t('packages.state.errorDescription', 'Please try again or contact support.')}
|
||
</Text>
|
||
</MobileCard>
|
||
) : (packages?.length ?? 0) === 0 ? (
|
||
<MobileCard>
|
||
<Text fontSize="$sm" fontWeight="700">
|
||
{t('packages.state.emptyTitle', 'Catalogue is empty')}
|
||
</Text>
|
||
<Text fontSize="$sm" color={muted}>
|
||
{t('packages.state.emptyDescription', 'No packages are currently available. Reach out to support to enable new offers.')}
|
||
</Text>
|
||
</MobileCard>
|
||
) : (
|
||
<YStack space="$3">
|
||
{packages?.map((pkg) => (
|
||
<PackageCard
|
||
key={pkg.id}
|
||
pkg={pkg}
|
||
selected={selectedId === pkg.id}
|
||
onSelect={() => handleSelect(pkg)}
|
||
/>
|
||
))}
|
||
</YStack>
|
||
)}
|
||
|
||
<MobileCard space="$2">
|
||
<Text fontSize="$sm" fontWeight="800">
|
||
{t('packages.step.title', 'Activate the right plan')}
|
||
</Text>
|
||
<Text fontSize="$sm" color={muted}>
|
||
{t('packages.step.description', 'Secure capacity for your next event. Upgrade at any time – only pay for what you need.')}
|
||
</Text>
|
||
</MobileCard>
|
||
|
||
<XStack space="$2">
|
||
<CTAButton
|
||
label={t('packages.cta.summary.button', 'Continue to summary')}
|
||
onPress={() => navigate(ADMIN_WELCOME_SUMMARY_PATH)}
|
||
disabled={!selectedId}
|
||
fullWidth={false}
|
||
/>
|
||
<CTAButton
|
||
label={t('packages.cta.billing.button', 'Open billing')}
|
||
tone="ghost"
|
||
onPress={() => navigate(adminPath('/mobile/billing#packages'))}
|
||
fullWidth={false}
|
||
/>
|
||
</XStack>
|
||
</OnboardingShell>
|
||
);
|
||
}
|
||
|
||
function PackageCard({
|
||
pkg,
|
||
selected,
|
||
onSelect,
|
||
}: {
|
||
pkg: Package;
|
||
selected: boolean;
|
||
onSelect: () => void;
|
||
}) {
|
||
const { t } = useTranslation('onboarding');
|
||
const { primary, border, accentSoft, muted } = useAdminTheme();
|
||
const badges = [
|
||
t('packages.card.badges.photos', { count: pkg.max_photos ?? 0, defaultValue: 'Unlimited photos' } as any),
|
||
t('packages.card.badges.guests', { count: pkg.max_guests ?? 0, defaultValue: 'Unlimited guests' } as any),
|
||
t('packages.card.badges.days', { count: pkg.gallery_days ?? 0, defaultValue: 'Unlimited days' } as any),
|
||
];
|
||
|
||
return (
|
||
<Pressable onPress={onSelect}>
|
||
<MobileCard borderColor={selected ? primary : border} space="$2">
|
||
<XStack alignItems="center" justifyContent="space-between">
|
||
<XStack alignItems="center" space="$2">
|
||
<XStack width={36} height={36} borderRadius={12} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
|
||
<PackageIcon size={18} color={primary} />
|
||
</XStack>
|
||
<YStack>
|
||
<Text fontSize="$sm" fontWeight="800">
|
||
{pkg.name}
|
||
</Text>
|
||
<Text fontSize="$xs" color={muted}>
|
||
{t('packages.card.description', 'Ready for your next event right away.')}
|
||
</Text>
|
||
</YStack>
|
||
</XStack>
|
||
<PillBadge tone={selected ? 'success' : 'muted'}>
|
||
{selected ? t('packages.card.selected', 'Selected') : t('packages.card.select', 'Select package')}
|
||
</PillBadge>
|
||
</XStack>
|
||
<XStack flexWrap="wrap" space="$2">
|
||
{badges.map((badge) => (
|
||
<PillBadge key={badge as any} tone="muted">
|
||
{badge as any}
|
||
</PillBadge>
|
||
))}
|
||
</XStack>
|
||
{selected ? (
|
||
<XStack alignItems="center" space="$1">
|
||
<Check size={14} color={primary} />
|
||
<Text fontSize="$xs" color={primary} fontWeight="700">
|
||
{t('packages.card.selected', 'Selected')}
|
||
</Text>
|
||
</XStack>
|
||
) : null}
|
||
</MobileCard>
|
||
</Pressable>
|
||
);
|
||
}
|