import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Check, ChevronRight, ShieldCheck, ShoppingBag, Sparkles, Star } from 'lucide-react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Checkbox } from '@tamagui/checkbox'; import toast from 'react-hot-toast'; import { MobileShell } from './components/MobileShell'; import { MobileCard, CTAButton, PillBadge, SkeletonCard } from './components/Primitives'; import { useAdminTheme } from './theme'; import { getPackages, createTenantPaddleCheckout, Package, getTenantPackagesOverview, TenantPackageSummary } from '../api'; import { getApiErrorMessage } from '../lib/apiError'; import { useQuery } from '@tanstack/react-query'; export default function MobilePackageShopPage() { const { t } = useTranslation('management'); const navigate = useNavigate(); const location = useLocation(); const { textStrong, muted, border, primary, surface, accentSoft, warningText } = useAdminTheme(); const [selectedPackage, setSelectedPackage] = React.useState(null); // 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 ( ); } if (selectedPackage) { return ( setSelectedPackage(null)} /> ); } // Merge and sort packages const sortedPackages = [...(catalog || [])].sort((a, b) => { // 1. Recommended feature first const aHasFeature = recommendedFeature && a.features?.[recommendedFeature]; const bHasFeature = recommendedFeature && b.features?.[recommendedFeature]; if (aHasFeature && !bHasFeature) return -1; if (!aHasFeature && bHasFeature) return 1; // 2. Inventory status (Owned packages later if they are fully used, but usually we want to show active stuff) // Actually, let's keep price sorting as secondary return a.price - b.price; }); return ( {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.')} {sortedPackages.map((pkg) => { const owned = inventory?.packages?.find(p => p.package_id === pkg.id); const isActive = inventory?.activePackage?.package_id === pkg.id; const isRecommended = recommendedFeature && pkg.features?.[recommendedFeature]; return ( setSelectedPackage(pkg)} /> ); })} ); } function PackageShopCard({ pkg, owned, isActive, isRecommended, onSelect }: { pkg: Package; owned?: TenantPackageSummary; isActive?: boolean; isRecommended?: any; onSelect: () => void }) { const { textStrong, muted, border, primary, accentSoft } = useAdminTheme(); const { t } = useTranslation('management'); const hasRemainingEvents = owned && (owned.remaining_events === null || owned.remaining_events > 0); const statusLabel = isActive ? t('shop.status.active', 'Active Plan') : owned ? (owned.remaining_events !== null ? t('shop.status.remaining', '{{count}} Events left', { count: owned.remaining_events }) : t('shop.status.owned', 'Purchased')) : null; return ( {pkg.name} {isRecommended && {t('shop.badges.recommended', 'Recommended')}} {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 */} {Object.entries(pkg.features || {}) .filter(([key, val]) => val === true && (!pkg.max_photos || key !== 'photos')) .slice(0, 3) .map(([key]) => ( )) } ); } function FeatureRow({ label }: { label: string }) { const { textStrong, primary } = useAdminTheme(); return ( {label} ) } function CheckoutConfirmation({ pkg, onCancel }: { pkg: Package; onCancel: () => void }) { const { t } = useTranslation('management'); const { textStrong, muted, border, primary, danger } = useAdminTheme(); const [agbAccepted, setAgbAccepted] = React.useState(false); const [withdrawalAccepted, setWithdrawalAccepted] = React.useState(false); const [busy, setBusy] = React.useState(false); const canProceed = agbAccepted && withdrawalAccepted; const handleCheckout = async () => { if (!canProceed || busy) return; setBusy(true); try { const { checkout_url } = await createTenantPaddleCheckout(pkg.id, { success_url: window.location.href, return_url: window.location.href, }); window.location.href = checkout_url; } catch (err) { toast.error(getApiErrorMessage(err, t('shop.errors.checkout', 'Checkout failed'))); setBusy(false); } }; 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.')} ); }