import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Check, ChevronRight, ShieldCheck, Sparkles, X } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Checkbox } from '@tamagui/checkbox'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { useAdminTheme } from './theme'; import { getPackages, Package, getTenantPackagesOverview, TenantPackageSummary } from '../api'; import { useQuery } from '@tanstack/react-query'; import { buildPackageComparisonRows, classifyPackageChange, getEnabledPackageFeatures, selectRecommendedPackageId, } from './lib/packageShop'; import { usePackageCheckout } from './hooks/usePackageCheckout'; export default function MobilePackageShopPage() { const { t } = useTranslation('management'); const navigate = useNavigate(); const location = useLocation(); const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); const [selectedPackage, setSelectedPackage] = React.useState(null); const [viewMode, setViewMode] = React.useState<'cards' | 'compare'>('cards'); // Extract recommended feature from URL const searchParams = new URLSearchParams(location.search); const recommendedFeature = searchParams.get('feature'); const forcedCatalogType = searchParams.get('type'); const { data: inventory, isLoading: loadingInventory } = useQuery({ queryKey: ['tenant-packages-overview'], queryFn: () => getTenantPackagesOverview({ force: true }), }); const catalogType: 'endcustomer' | 'reseller' = forcedCatalogType === 'endcustomer' || forcedCatalogType === 'reseller' ? forcedCatalogType : inventory?.activePackage?.package_type === 'reseller' || (inventory?.packages ?? []).some((entry) => entry.package_type === 'reseller') ? 'reseller' : 'endcustomer'; const { data: catalog, isLoading: loadingCatalog } = useQuery({ queryKey: ['packages', catalogType], queryFn: () => getPackages(catalogType), }); const isLoading = loadingCatalog || loadingInventory; if (isLoading) { return ( navigate(-1)} activeTab="profile" > ); } if (selectedPackage) { return ( setSelectedPackage(null)} /> ); } const activePackageId = inventory?.activePackage?.package_id ?? null; const activeCatalogPackage = (catalog ?? []).find((pkg) => pkg.id === activePackageId) ?? null; const recommendedPackageId = catalogType === 'reseller' ? null : selectRecommendedPackageId(catalog ?? [], recommendedFeature, activeCatalogPackage); // Merge and sort packages const sortedPackages = [...(catalog || [])].sort((a, b) => { if (recommendedPackageId) { if (a.id === recommendedPackageId && b.id !== recommendedPackageId) return -1; if (b.id === recommendedPackageId && a.id !== recommendedPackageId) return 1; } return a.price - b.price; }); const packageEntries = sortedPackages.map((pkg) => { const ownedEntries = (inventory?.packages ?? []).filter((entry) => entry.package_id === pkg.id && entry.active); const owned = ownedEntries.length ? aggregateOwnedEntries(ownedEntries) : undefined; const isActive = catalogType === 'reseller' ? false : inventory?.activePackage?.package_id === pkg.id; const isRecommended = recommendedPackageId ? pkg.id === recommendedPackageId : false; const { isUpgrade, isDowngrade } = catalogType === 'reseller' ? { isUpgrade: false, isDowngrade: false } : classifyPackageChange(pkg, activeCatalogPackage); return { pkg, owned, isActive, isRecommended, isUpgrade, isDowngrade, }; }); return ( navigate(-1)} activeTab="profile" > {catalogType !== 'reseller' && recommendedFeature && ( {t('shop.recommendationTitle', 'Recommended for you')} {t('shop.recommendationBody', 'The highlighted package includes the feature you requested.')} )} {catalogType === 'reseller' ? t('shop.partner.subtitle', 'Kaufe Event-Kontingente, um mehrere Events mit unseren Services umzusetzen.') : t('shop.subtitle', 'Choose a package to unlock more features and limits.')} {packageEntries.length > 1 ? ( setViewMode('cards')} style={{ flex: 1 }} /> setViewMode('compare')} style={{ flex: 1 }} /> ) : null} {viewMode === 'compare' ? ( setSelectedPackage(pkg)} catalogType={catalogType} /> ) : ( packageEntries.map((entry) => ( setSelectedPackage(entry.pkg)} /> )) )} ); } function PackageShopCard({ pkg, owned, isActive, isRecommended, isUpgrade, isDowngrade, catalogType, onSelect }: { pkg: Package; owned?: TenantPackageSummary; isActive?: boolean; isRecommended?: any; isUpgrade?: boolean; isDowngrade?: boolean; catalogType: 'endcustomer' | 'reseller'; onSelect: () => void }) { const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); const { t } = useTranslation('management'); const isResellerCatalog = catalogType === 'reseller'; const statusLabel = getPackageStatusLabel({ t, isActive, owned }); const isSubdued = Boolean(!isResellerCatalog && (isDowngrade || !isUpgrade) && !isActive); const canSelect = isResellerCatalog ? Boolean(pkg.paddle_price_id) : canSelectPackage(isUpgrade, isActive); const includedTierLabel = resolveIncludedTierLabel(t, pkg.included_package_slug ?? null); return ( {pkg.name} {isRecommended && {t('shop.badges.recommended', 'Recommended')}} {!isResellerCatalog && isUpgrade && !isActive ? ( {t('shop.badges.upgrade', 'Upgrade')} ) : null} {!isResellerCatalog && isDowngrade && !isActive ? ( {t('shop.badges.downgrade', 'Downgrade')} ) : null} {!isResellerCatalog && isActive ? {t('shop.badges.active', 'Active')} : null} {new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(pkg.price)} {statusLabel && ( • {statusLabel} )} {isResellerCatalog ? ( <> {includedTierLabel ? ( ) : null} {typeof pkg.max_events_per_year === 'number' ? ( ) : null} ) : ( <> {pkg.max_photos ? ( ) : ( )} {pkg.gallery_days ? ( ) : null} )} {/* Render specific feature if it was requested */} {!isResellerCatalog ? getEnabledPackageFeatures(pkg) .filter((key) => !pkg.max_photos || key !== 'photos') .slice(0, 3) .map((key) => ( )) : null} ); } function FeatureRow({ label }: { label: string }) { const { textStrong, primary } = useAdminTheme(); return ( {label} ) } type PackageEntry = { pkg: Package; owned?: TenantPackageSummary; isActive: boolean; isRecommended: boolean; isUpgrade: boolean; isDowngrade: boolean; }; function PackageShopCompareView({ entries, onSelect, catalogType, }: { entries: PackageEntry[]; onSelect: (pkg: Package) => void; catalogType: 'endcustomer' | 'reseller'; }) { const { t } = useTranslation('management'); const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); const comparisonRows = buildPackageComparisonRows(entries.map((entry) => entry.pkg)); const labelWidth = 140; const columnWidth = 150; const rows = [ { id: 'meta.plan', type: 'meta' as const, label: t('shop.compare.headers.plan', 'Plan') }, { id: 'meta.price', type: 'meta' as const, label: t('shop.compare.headers.price', 'Price') }, ...comparisonRows, ]; const renderRowLabel = (row: typeof rows[number]) => { if (row.type === 'meta') { return row.label; } if (row.type === 'limit') { if (row.limitKey === 'max_photos') { return t('shop.compare.rows.photos', 'Photos'); } if (row.limitKey === 'max_guests') { return t('shop.compare.rows.guests', 'Guests'); } if (row.limitKey === 'max_events_per_year') { return t('shop.partner.compare.rows.events', 'Events im Kontingent'); } return t('shop.compare.rows.days', 'Gallery days'); } if (row.type === 'value') { if (row.valueKey === 'included_package_slug') { return t('shop.partner.compare.rows.includedTier', 'Inklusive Event-Level'); } } return t(`shop.features.${row.featureKey}`, row.featureKey); }; const formatLimitValue = (value: number | null) => { if (value === null) { return t('shop.compare.values.unlimited', 'Unlimited'); } return new Intl.NumberFormat().format(value); }; return ( {t('shop.compare.title', 'Compare plans')} {t('shop.compare.helper', 'Swipe to compare packages side by side.')} {rows.map((row) => ( {renderRowLabel(row)} {entries.map((entry) => { const cellBackground = entry.isRecommended ? accentSoft : entry.isActive ? '$green1' : undefined; let content: React.ReactNode = null; if (row.type === 'meta') { if (row.id === 'meta.plan') { const statusLabel = getPackageStatusLabel({ t, isActive: entry.isActive, owned: entry.owned }); content = ( {entry.pkg.name} {entry.isRecommended ? ( {t('shop.badges.recommended', 'Recommended')} ) : null} {catalogType !== 'reseller' && entry.isUpgrade && !entry.isActive ? ( {t('shop.badges.upgrade', 'Upgrade')} ) : null} {catalogType !== 'reseller' && entry.isDowngrade && !entry.isActive ? ( {t('shop.badges.downgrade', 'Downgrade')} ) : null} {catalogType !== 'reseller' && entry.isActive ? ( {t('shop.badges.active', 'Active')} ) : null} {statusLabel ? ( {statusLabel} ) : null} ); } else if (row.id === 'meta.price') { content = ( {new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(entry.pkg.price)} ); } } else if (row.type === 'limit') { const value = entry.pkg[row.limitKey] ?? null; content = ( {formatLimitValue(value)} ); } else if (row.type === 'value') { content = ( {resolveIncludedTierLabel(t, entry.pkg.included_package_slug ?? null) ?? t('shop.partner.compare.values.unknown', '—')} ); } else if (row.type === 'feature') { const enabled = getEnabledPackageFeatures(entry.pkg).includes(row.featureKey); content = ( {enabled ? ( ) : ( )} {enabled ? t('shop.compare.values.included', 'Included') : t('shop.compare.values.notIncluded', 'Not included')} ); } return ( {content} ); })} ))} {entries.map((entry) => { const isResellerCatalog = catalogType === 'reseller'; const canSelect = isResellerCatalog ? Boolean(entry.pkg.paddle_price_id) : canSelectPackage(entry.isUpgrade, entry.isActive); const label = isResellerCatalog ? canSelect ? t('shop.partner.buy', 'Kaufen') : t('shop.partner.unavailable', 'Nicht verfügbar') : entry.isActive ? t('shop.manage', 'Manage Plan') : entry.isUpgrade ? t('shop.select', 'Select') : t('shop.selectDisabled', 'Not available'); return ( onSelect(entry.pkg) : undefined} disabled={!canSelect} tone={ catalogType === 'reseller' ? canSelect ? 'primary' : 'ghost' : entry.isActive || entry.isDowngrade ? 'ghost' : 'primary' } /> ); })} ); } function getPackageStatusLabel({ t, isActive, owned, }: { t: (key: string, fallback?: string, options?: Record) => string; isActive?: boolean; owned?: TenantPackageSummary; }): string | null { if (isActive) { return t('shop.status.active', 'Active Plan'); } if (owned) { return owned.remaining_events !== null ? t('shop.status.remaining', '{{count}} Events left', { count: owned.remaining_events }) : t('shop.status.owned', 'Purchased'); } return null; } function canSelectPackage(isUpgrade?: boolean, isActive?: boolean): boolean { return Boolean(isActive || isUpgrade); } function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () => void }) { const { t } = useTranslation('management'); const { textStrong, muted, border, primary } = useAdminTheme(); const [agbAccepted, setAgbAccepted] = React.useState(false); const [withdrawalAccepted, setWithdrawalAccepted] = React.useState(false); const { busy, startCheckout } = usePackageCheckout(); const canProceed = agbAccepted && withdrawalAccepted; const handleCheckout = async () => { if (!canProceed || busy) return; await startCheckout(pkg.id); }; const subtitle = pkg.type === 'reseller' ? t('shop.partner.confirmSubtitle', 'Du kaufst:') : t('shop.confirmSubtitle', 'You are upgrading to:'); return ( {subtitle} {pkg.name} {new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(pkg.price)} {t('shop.legalTitle', 'Terms & Conditions')} setAgbAccepted(!!checked)} > setAgbAccepted(!agbAccepted)}> {t('shop.legal.agb', 'I agree to the Terms and Conditions and Privacy Policy.')} setWithdrawalAccepted(!!checked)} > setWithdrawalAccepted(!withdrawalAccepted)}> {t('shop.legal.withdrawal', 'I agree that the contract execution begins immediately and I lose my right of withdrawal.')} ); } function aggregateOwnedEntries(entries: TenantPackageSummary[]): TenantPackageSummary { const remainingTotal = entries.reduce( (total, entry) => total + (typeof entry.remaining_events === 'number' ? entry.remaining_events : 0), 0 ); const usedTotal = entries.reduce( (total, entry) => total + (typeof entry.used_events === 'number' ? entry.used_events : 0), 0 ); return { ...entries[0], used_events: usedTotal, remaining_events: Number.isFinite(remainingTotal) ? remainingTotal : entries[0].remaining_events, }; } function resolveIncludedTierLabel( t: (key: string, fallback?: string, options?: Record) => string, slug: string | null ): string | null { if (!slug) { return null; } if (slug === 'starter') { return t('shop.partner.tiers.starter', 'Starter'); } if (slug === 'standard') { return t('shop.partner.tiers.standard', 'Standard'); } if (slug === 'pro') { return t('shop.partner.tiers.premium', 'Premium'); } return slug; }