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 { data: catalog, isLoading: loadingCatalog } = useQuery({ queryKey: ['packages', 'endcustomer'], queryFn: () => getPackages('endcustomer'), }); const { data: inventory, isLoading: loadingInventory } = useQuery({ queryKey: ['tenant-packages-overview'], queryFn: () => getTenantPackagesOverview({ force: true }), }); 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 = 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 owned = inventory?.packages?.find((entry) => entry.package_id === pkg.id); const isActive = inventory?.activePackage?.package_id === pkg.id; const isRecommended = recommendedPackageId ? pkg.id === recommendedPackageId : false; const { isUpgrade, isDowngrade } = classifyPackageChange(pkg, activeCatalogPackage); return { pkg, owned, isActive, isRecommended, isUpgrade, isDowngrade, }; }); return ( navigate(-1)} activeTab="profile"> {recommendedFeature && ( {t('shop.recommendationTitle', 'Recommended for you')} {t('shop.recommendationBody', 'The highlighted package includes the feature you requested.')} )} {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)} /> ) : ( packageEntries.map((entry) => ( setSelectedPackage(entry.pkg)} /> )) )} ); } function PackageShopCard({ pkg, owned, isActive, isRecommended, isUpgrade, isDowngrade, onSelect }: { pkg: Package; owned?: TenantPackageSummary; isActive?: boolean; isRecommended?: any; isUpgrade?: boolean; isDowngrade?: boolean; onSelect: () => void }) { const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); const { t } = useTranslation('management'); const statusLabel = getPackageStatusLabel({ t, isActive, owned }); const isSubdued = Boolean((isDowngrade || !isUpgrade) && !isActive); const canSelect = canSelectPackage(isUpgrade, isActive); return ( {pkg.name} {isRecommended && {t('shop.badges.recommended', 'Recommended')}} {isUpgrade && !isActive ? {t('shop.badges.upgrade', 'Upgrade')} : null} {isDowngrade && !isActive ? {t('shop.badges.downgrade', 'Downgrade')} : null} {isActive && {t('shop.badges.active', 'Active')}} {new Intl.NumberFormat(undefined, { style: 'currency', currency: 'EUR' }).format(pkg.price)} {statusLabel && ( • {statusLabel} )} {pkg.max_photos ? ( ) : ( )} {pkg.gallery_days ? ( ) : null} {/* Render specific feature if it was requested */} {getEnabledPackageFeatures(pkg) .filter((key) => !pkg.max_photos || key !== 'photos') .slice(0, 3) .map((key) => ( ))} ); } 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, }: { entries: PackageEntry[]; onSelect: (pkg: Package) => void; }) { 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'); } return t('shop.compare.rows.days', 'Gallery days'); } 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} {entry.isUpgrade && !entry.isActive ? ( {t('shop.badges.upgrade', 'Upgrade')} ) : null} {entry.isDowngrade && !entry.isActive ? ( {t('shop.badges.downgrade', 'Downgrade')} ) : null} {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 === '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 canSelect = canSelectPackage(entry.isUpgrade, entry.isActive); const label = 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={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); }; return ( {t('shop.confirmSubtitle', 'You are upgrading to:')} {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.')} ); }