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 ? (
-
+
) : (