import { useEffect, useRef, useState } from 'react'; import { Animated, Dimensions, Modal, ScrollView, Text, 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 { Button } from '../Button'; 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}