diff --git a/apps/rebreak-native/app/devices.tsx b/apps/rebreak-native/app/devices.tsx index 7ca1ace..872407e 100644 --- a/apps/rebreak-native/app/devices.tsx +++ b/apps/rebreak-native/app/devices.tsx @@ -15,6 +15,16 @@ import { MenuView } from '@react-native-menu/menu'; import { useTranslation } from 'react-i18next'; import { useColors } from '../lib/theme'; import { useDevicesStore, type UserDevice } from '../stores/devices'; + +function formatCountdown(isoTarget: string): string { + const ms = new Date(isoTarget).getTime() - Date.now(); + if (ms <= 0) return '0min'; + const totalMin = Math.floor(ms / 60_000); + const h = Math.floor(totalMin / 60); + const m = totalMin % 60; + if (h > 0) return `${h}h ${m}min`; + return `${m}min`; +} import { useProtectedDevicesStore, type ProtectedDevice } from '../stores/protectedDevices'; import { useProtectedDevicesRealtime } from '../hooks/useProtectedDevicesRealtime'; import { useUserPlan } from '../hooks/useUserPlan'; @@ -107,18 +117,28 @@ function StatusBadge({ status }: { status: ProtectedDevice['status'] }) { ); } -// ─── Mobile Device Row (existing) ──────────────────────────────────────────── +// ─── Mobile Device Row ──────────────────────────────────────────────────────── function MobileDeviceRow({ device, onRemove, + onRequestRelease, + onCancelRelease, }: { device: UserDevice; onRemove: (id: string) => void; + onRequestRelease: (id: string) => void; + onCancelRelease: (id: string) => void; }) { const { t } = useTranslation(); const colors = useColors(); + const isBound = !!device.boundToPlan; + const releaseAt = device.releaseRequestedAt + ? new Date(new Date(device.releaseRequestedAt).getTime() + 24 * 60 * 60 * 1000).toISOString() + : null; + const releaseActive = !!releaseAt && new Date(releaseAt).getTime() > Date.now(); + function confirmRemove() { Alert.alert( t('settings.devices_remove_title'), @@ -134,6 +154,36 @@ function MobileDeviceRow({ ); } + function confirmRequestRelease() { + Alert.alert( + t('devices.release_request_title'), + t('devices.release_request_body'), + [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('devices.release_request_confirm'), + style: 'destructive', + onPress: () => onRequestRelease(device.id), + }, + ] + ); + } + + function confirmCancelRelease() { + Alert.alert( + t('devices.release_cancel_confirm'), + t('devices.release_cancel_body'), + [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('devices.release_cancel_cta'), + style: 'destructive', + onPress: () => onCancelRelease(device.id), + }, + ] + ); + } + const deviceName = device.model ?? device.name ?? device.platform; const footerText = `${formatLastSeen(device.lastSeenAt, t)} · ${t('settings.devices_since')} ${formatSince(device.createdAt)}`; @@ -193,22 +243,64 @@ function MobileDeviceRow({ ) : null} + {isBound && !releaseActive ? ( + + + + {t('devices.bound_badge')} + + + ) : null} - {footerText} + {releaseActive && releaseAt + ? t('devices.release_countdown', { remaining: formatCountdown(releaseAt) }) + : footerText} - {!device.isCurrent ? ( + {device.isCurrent ? null : releaseActive ? ( + + + + ) : isBound ? ( + + + + ) : ( - ) : null} + )} ); } @@ -384,6 +476,8 @@ export default function DevicesScreen() { loading: mobileLoading, loadDevices, removeDevice: removeMobileDevice, + requestRelease, + cancelRelease, } = useDevicesStore(); const { @@ -467,7 +561,12 @@ export default function DevicesScreen() { ) : currentDevice ? ( - + ) : (