Files
fotospiel-app/resources/js/admin/mobile/welcome/WelcomePackagesPage.tsx
Codex Agent cf73f408b2 Navigation now feels more “app‑like” with
stateful tabs and reliable back behavior, and a full onboarding flow is wired in with conditional package selection
  (skips when an active package exists).

  What changed

  - Added per‑tab history + back navigation fallback to make tab switching/Back feel native (resources/js/admin/mobile/
    lib/tabHistory.ts, resources/js/admin/mobile/hooks/useBackNavigation.ts, resources/js/admin/mobile/hooks/
    useMobileNav.ts, resources/js/admin/mobile/components/MobileShell.tsx + updates across mobile pages).
  - Implemented onboarding flow pages + shared shell, and wired new routes/prefetch (resources/js/admin/mobile/welcome/
    WelcomeLandingPage.tsx, resources/js/admin/mobile/welcome/WelcomePackagesPage.tsx, resources/js/admin/mobile/
    welcome/WelcomeSummaryPage.tsx, resources/js/admin/mobile/welcome/WelcomeEventPage.tsx, resources/js/admin/mobile/
    components/OnboardingShell.tsx, resources/js/admin/router.tsx, resources/js/admin/mobile/prefetch.ts).
  - Conditional package step: packages page redirects to event setup if activePackage exists; selection stored locally
    for summary (resources/js/admin/mobile/lib/onboardingSelection.ts, resources/js/admin/mobile/welcome/
    WelcomePackagesPage.tsx).
  - Added a “Start welcome journey” CTA in the empty dashboard state (resources/js/admin/mobile/DashboardPage.tsx).
  - Added translations for onboarding shell + selected package + dashboard CTA (resources/js/admin/i18n/locales/en/
    onboarding.json, resources/js/admin/i18n/locales/de/onboarding.json, resources/js/admin/i18n/locales/en/
    management.json, resources/js/admin/i18n/locales/de/management.json).
  - Tests for new helpers/hooks (resources/js/admin/mobile/lib/tabHistory.test.ts, resources/js/admin/mobile/lib/
    onboardingSelection.test.ts, resources/js/admin/mobile/hooks/useBackNavigation.test.tsx).
2025-12-28 19:51:57 +01:00

181 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
export default function WelcomePackagesPage() {
const navigate = useNavigate();
const { t } = useTranslation('onboarding');
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="#6b7280">
{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="#6b7280">
{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="#6b7280">
{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="#6b7280">
{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 badges = [
t('packages.card.badges.photos', { count: pkg.max_photos ?? t('summary.details.infinity', '∞') }),
t('packages.card.badges.guests', { count: pkg.max_guests ?? t('summary.details.infinity', '∞') }),
t('packages.card.badges.days', { count: pkg.gallery_days ?? t('summary.details.infinity', '∞') }),
];
return (
<Pressable onPress={onSelect}>
<MobileCard borderColor={selected ? '#2563eb' : '#e5e7eb'} space="$2">
<XStack alignItems="center" justifyContent="space-between">
<XStack alignItems="center" space="$2">
<XStack width={36} height={36} borderRadius={12} backgroundColor="#e0f2fe" alignItems="center" justifyContent="center">
<PackageIcon size={18} color="#0ea5e9" />
</XStack>
<YStack>
<Text fontSize="$sm" fontWeight="800">
{pkg.name}
</Text>
<Text fontSize="$xs" color="#6b7280">
{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} tone="muted">
{badge}
</PillBadge>
))}
</XStack>
{selected ? (
<XStack alignItems="center" space="$1">
<Check size={14} color="#2563eb" />
<Text fontSize="$xs" color="#2563eb" fontWeight="700">
{t('packages.card.selected', 'Selected')}
</Text>
</XStack>
) : null}
</MobileCard>
</Pressable>
);
}