import { useEffect, useRef, useState } from 'react'; import { Animated, Dimensions, Modal, ScrollView, Text, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { apiFetch } from '../../lib/api'; import { useColors } from '../../lib/theme'; import type { Plan } from '../../hooks/useMe'; export type ChangePreviewItem = { resource: string; current: number | string; newLimit: number | string; overBy: number; action: 'keep' | 'limited' | 'paused' | 'grace_then_off' | 'degraded' | 'unlocked'; detail: string; graceUntilDays?: number; }; export type ChangePreview = { from: Plan; to: Plan; direction: 'upgrade' | 'downgrade' | 'same'; gains: string[]; keeps: string[]; changes: ChangePreviewItem[]; }; type Props = { visible: boolean; targetPlan: Plan; onConfirm: () => void; onClose: () => void; }; const SCREEN_HEIGHT = Dimensions.get('window').height; const PLAN_LABEL: Record = { free: 'Free', pro: 'Pro', legend: 'Legend', }; function ActionChip({ action }: { action: ChangePreviewItem['action'] }) { const { t } = useTranslation(); const configs: Record = { keep: { label: t('plan.change.action_keep'), fg: '#16a34a', bg: 'rgba(22,163,74,0.1)' }, limited: { label: t('plan.change.action_limited'), fg: '#d97706', bg: 'rgba(217,119,6,0.1)' }, paused: { label: t('plan.change.action_paused'), fg: '#737373', bg: 'rgba(115,115,115,0.1)' }, grace_then_off:{ label: t('plan.change.action_grace'), fg: '#d97706', bg: 'rgba(217,119,6,0.1)' }, degraded: { label: t('plan.change.action_degraded'), fg: '#dc2626', bg: 'rgba(220,38,38,0.1)' }, unlocked: { label: t('plan.change.action_unlocked'), fg: '#16a34a', bg: 'rgba(22,163,74,0.1)' }, }; const c = configs[action]; return ( {c.label} ); } export function PlanChangeSheet({ visible, targetPlan, onConfirm, onClose }: Props) { const { t } = useTranslation(); const colors = useColors(); const insets = useSafeAreaInsets(); const translateY = useRef(new Animated.Value(SCREEN_HEIGHT)).current; const [preview, setPreview] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!visible) return; setPreview(null); setError(null); setLoading(true); apiFetch(`/api/plan/change-preview?to=${targetPlan}`) .then(setPreview) .catch((e: Error) => setError(e.message)) .finally(() => setLoading(false)); }, [visible, targetPlan]); useEffect(() => { Animated.timing(translateY, { toValue: visible ? 0 : SCREEN_HEIGHT, duration: 320, useNativeDriver: true, }).start(); }, [visible, translateY]); const isDowngrade = preview?.direction === 'downgrade'; return ( {/* Handle */} {loading ? ( {t('common.loading')} ) : error ? ( {error} {t('common.back')} ) : preview ? ( ) : null} ); } type ContentProps = { preview: ChangePreview; targetPlan: Plan; isDowngrade: boolean; colors: import('../../lib/theme').ColorScheme; onConfirm: () => void; onClose: () => void; t: (k: string, opts?: Record) => string; }; function SheetContent({ preview, targetPlan, isDowngrade, colors, onConfirm, onClose, t }: ContentProps) { const fromLabel = PLAN_LABEL[preview.from]; const toLabel = PLAN_LABEL[targetPlan]; return ( <> {/* Header */} {isDowngrade ? t('plan.change.header_downgrade', { from: fromLabel, to: toLabel }) : t('plan.change.header_upgrade', { to: toLabel })} {/* Downgrade: Beruhigung zuerst */} {isDowngrade && ( {t('plan.change.downgrade_reassurance')} {preview.keeps.length > 0 && ( {preview.keeps.map((k, i) => ( {k} ))} )} )} {/* Upgrade: gains */} {!isDowngrade && preview.gains.length > 0 && ( {t('plan.change.section_gains')} {preview.gains.map((g, i) => ( {g} ))} )} {/* Upgrade: keeps */} {!isDowngrade && preview.keeps.length > 0 && ( {t('plan.change.section_keeps')} {preview.keeps.map((k, i) => ( {k} ))} )} {/* Downgrade: changes list */} {isDowngrade && preview.changes.length > 0 && ( {t('plan.change.section_changes')} {preview.changes.map((c, i) => ( {c.resource} {c.detail} {c.graceUntilDays != null && c.graceUntilDays > 0 && ( {t('plan.change.grace_days', { count: c.graceUntilDays })} )} ))} )} {/* Downgrade: was nicht passiert */} {isDowngrade && ( {t('plan.change.downgrade_no_delete_title')} {t('plan.change.downgrade_no_delete_body')} )} {/* Upgrade: Abrechnungs-Hinweis statt Kauf-Button */} {!isDowngrade && ( {t('plan.change.billing_hint')} )} {/* Downgrade: Recovery-Sicherheitssatz */} {isDowngrade && ( {t('plan.change.downgrade_recovery_note')} )} {/* CTAs */} {isDowngrade ? t('plan.change.cta_confirm_downgrade') : t('plan.change.cta_confirm_upgrade')} {isDowngrade && ( {t('plan.change.cta_stay', { plan: PLAN_LABEL[preview.from] })} )} {!isDowngrade && ( {t('common.cancel')} )} ); }