diff --git a/apps/rebreak-native/app/devices.tsx b/apps/rebreak-native/app/devices.tsx
index 6bc25dc..303a1c3 100644
--- a/apps/rebreak-native/app/devices.tsx
+++ b/apps/rebreak-native/app/devices.tsx
@@ -32,7 +32,8 @@ import { useUserDevicesRealtime } from '../hooks/useUserDevicesRealtime';
import { useUserPlan } from '../hooks/useUserPlan';
import { AppHeader } from '../components/AppHeader';
import { MagicSheet } from '../components/devices/MagicSheet';
-import { DeviceProgressBar } from '../components/devices/DeviceProgressBar';
+import { DeviceSlotDonut } from '../components/devices/DeviceSlotDonut';
+import { DeviceStatusPill } from '../components/devices/DeviceStatusPill';
import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet';
import { deviceImage } from '../components/devices/deviceIcon';
@@ -270,19 +271,10 @@ function MobileDeviceRow({
) : null}
-
- {releaseActive && releaseAt
- ? t('devices.release_countdown', { remaining: formatCountdown(releaseAt) })
- : footerText}
-
+
@@ -399,22 +391,17 @@ function ProtectedDeviceRow({
>
{device.label}
-
-
- {device.status === 'degraded'
- ? t('plan_limit.device_degraded_body')
- : `${t('settings.devices_since')} ${formatSince(device.createdAt)}`}
-
+
@@ -592,18 +579,20 @@ export default function DevicesScreen() {
>
{subtitle}
- = mobileLimit}
- label={t('devices.progress_mobile')}
- />
-
+
+ = mobileLimit}
+ label={t('devices.progress_mobile')}
+ />
+
+
{/* Unified devices section: Mobile zuerst, dann Desktop */}
diff --git a/apps/rebreak-native/components/devices/DeviceSlotDonut.tsx b/apps/rebreak-native/components/devices/DeviceSlotDonut.tsx
new file mode 100644
index 0000000..632c545
--- /dev/null
+++ b/apps/rebreak-native/components/devices/DeviceSlotDonut.tsx
@@ -0,0 +1,108 @@
+import { useEffect, useRef, useState } from 'react';
+import { Animated, Easing, Text, View } from 'react-native';
+import Svg, { Circle } from 'react-native-svg';
+import { useColors } from '../../lib/theme';
+
+const SIZE = 104;
+const STROKE = 9;
+const R = (SIZE - STROKE) / 2;
+const C = 2 * Math.PI * R;
+
+/**
+ * Voller Progress-Ring (kein Half-Donut) für belegte/freie Geräte-Slots.
+ * Mit react-native-svg (schon im Projekt) statt neuer Lib — animiert über
+ * strokeDashoffset. Zwei nebeneinander (Mobil + Computer).
+ */
+export function DeviceSlotDonut({
+ count,
+ max,
+ atLimit,
+ label,
+}: {
+ count: number;
+ max: number;
+ atLimit: boolean;
+ label: string;
+}) {
+ const colors = useColors();
+ const ratio = max > 0 ? Math.min(count / max, 1) : 0;
+ const fill = atLimit ? colors.brandOrange : colors.success;
+
+ const anim = useRef(new Animated.Value(0)).current;
+ const [progress, setProgress] = useState(0);
+
+ useEffect(() => {
+ anim.setValue(0);
+ const l = anim.addListener(({ value }) => setProgress(value));
+ Animated.timing(anim, {
+ toValue: 1,
+ duration: 950,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: false,
+ }).start();
+ return () => anim.removeListener(l);
+ }, [ratio, anim]);
+
+ const offset = C * (1 - ratio * progress);
+
+ return (
+
+
+
+
+
+ {count}
+
+ /{max}
+
+
+
+
+
+
+ {label}
+
+
+ );
+}
diff --git a/apps/rebreak-native/components/devices/DeviceStatusPill.tsx b/apps/rebreak-native/components/devices/DeviceStatusPill.tsx
new file mode 100644
index 0000000..64314c8
--- /dev/null
+++ b/apps/rebreak-native/components/devices/DeviceStatusPill.tsx
@@ -0,0 +1,53 @@
+import { Text, View } from 'react-native';
+import { useTranslation } from 'react-i18next';
+import { useColors } from '../../lib/theme';
+
+export type DeviceStatusKind = 'online' | 'cooldown' | 'unprotected' | 'pending';
+
+/**
+ * Status-Zeile in der Geräte-Liste: farbiger Punkt + Text.
+ * - online → geschützt/verbunden (grün)
+ * - cooldown → Pause beantragt, läuft ab (amber, mit Restdauer)
+ * - unprotected → Schutz aus (rot, mit Dauer)
+ * - pending → wartet auf Aktivierung (amber)
+ */
+export function DeviceStatusPill({
+ kind,
+ durationText,
+}: {
+ kind: DeviceStatusKind;
+ durationText?: string;
+}) {
+ const { t } = useTranslation();
+ const colors = useColors();
+
+ const cfg: Record = {
+ online: { color: colors.success, label: t('devices.status_online') },
+ cooldown: {
+ color: '#f59e0b',
+ label: durationText
+ ? t('devices.status_cooldown', { time: durationText })
+ : t('devices.status_cooldown_short'),
+ },
+ unprotected: {
+ color: colors.error,
+ label: durationText
+ ? t('devices.status_unprotected_since', { time: durationText })
+ : t('devices.status_unprotected'),
+ },
+ pending: { color: '#f59e0b', label: t('devices.status_pending') },
+ };
+ const c = cfg[kind];
+
+ return (
+
+
+
+ {c.label}
+
+
+ );
+}
diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json
index 2af9be3..3caadab 100644
--- a/apps/rebreak-native/locales/de.json
+++ b/apps/rebreak-native/locales/de.json
@@ -664,6 +664,24 @@
"title": "Wähle deinen Alias",
"body": "Das ist dein einziger Name in rebreak. Niemand sieht deine Mail oder deinen echten Namen.",
"finish": "Verstanden"
+ },
+ "protection_confirm": {
+ "checkbox": "Ich hab’s verstanden",
+ "cta": "Weiter",
+ "vpn_title": "VPN-Erlaubnis",
+ "vpn_body": "Gleich fragt Android nach VPN-Erlaubnis. Tipp im Dialog auf „Zulassen/OK“ — das ist kein echtes VPN, der Filter läuft lokal auf deinem Gerät.",
+ "deviceadmin_title": "Geräteschutz",
+ "deviceadmin_body": "Im nächsten Dialog auf „Aktivieren“ tippen. Damit ist der Schutz schon ab dem Neustart aktiv — es werden keine weiteren Rechte angefragt.",
+ "applock_title": "App-Sperre",
+ "applock_body": "Achtung im nächsten Dialog: Tipp den UNTEREN Button — nicht den blauen. Nur so wird die App-Sperre aktiv.",
+ "urlfilter_title": "Inhaltsfilter",
+ "urlfilter_body": "Gleich fragt iOS nach Erlaubnis für den Filter. Tipp auf „Erlauben“.",
+ "a11y_title": "Schutz-Wächter (Bedienungshilfen)",
+ "a11y_body": "Dieser Schritt ist etwas länger — ich führ dich durch. Über die Bedienungshilfen schützt ReBreak deine Einstellungen vor versehentlichem Abschalten.",
+ "a11y_step1": "Zuerst fragt Android nach „Über anderen Apps anzeigen“ — tippe Erlauben (brauchen wir, um dir den nächsten Schritt einzublenden).",
+ "a11y_step2": "Dann öffnet sich die Bedienungshilfen-Liste — such „ReBreak“.",
+ "a11y_step3": "Tippe ReBreak an und schalte den Schalter ein. Komm danach zurück zur App.",
+ "a11y_indicator": "Hier ReBreak antippen & einschalten"
}
},
"protection_onboarding": {
@@ -1331,6 +1349,11 @@
"status_pending": "Bereit zum Installieren",
"status_active": "Aktiv",
"status_revoked": "Entfernt",
+ "status_online": "Online",
+ "status_cooldown": "Cooldown · noch %{time}",
+ "status_cooldown_short": "Cooldown",
+ "status_unprotected": "Ungeschützt",
+ "status_unprotected_since": "Ungeschützt · seit %{time}",
"label_placeholder": "z.B. MacBook Pro",
"label_default": "MacBook Pro",
"label_question": "Wie soll der Mac heißen?",
diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json
index 61ad35a..1ee03be 100644
--- a/apps/rebreak-native/locales/en.json
+++ b/apps/rebreak-native/locales/en.json
@@ -664,6 +664,24 @@
"title": "Pick your alias",
"body": "This is your only name on rebreak. No one sees your email or real name.",
"finish": "Got it"
+ },
+ "protection_confirm": {
+ "checkbox": "I understand",
+ "cta": "Continue",
+ "vpn_title": "VPN permission",
+ "vpn_body": "Android will now ask for VPN permission. Tap “Allow/OK” in the dialog — it’s not a real VPN, the filter runs locally on your device.",
+ "deviceadmin_title": "Device protection",
+ "deviceadmin_body": "Tap “Activate” in the next dialog. This keeps protection on right from restart — no further rights are requested.",
+ "applock_title": "App lock",
+ "applock_body": "Heads up in the next dialog: tap the BOTTOM button — not the blue one. That’s the only way the app lock turns on.",
+ "urlfilter_title": "Content filter",
+ "urlfilter_body": "iOS will now ask permission for the filter. Tap “Allow”.",
+ "a11y_title": "Protection guard (Accessibility)",
+ "a11y_body": "This step is a little longer — I’ll guide you. Via Accessibility, ReBreak protects your settings from being switched off by accident.",
+ "a11y_step1": "First Android asks for “Display over other apps” — tap Allow (we need it to show you the next step on screen).",
+ "a11y_step2": "Then the Accessibility list opens — find “ReBreak”.",
+ "a11y_step3": "Tap ReBreak and turn the switch on. Then come back to the app.",
+ "a11y_indicator": "Tap ReBreak here & switch it on"
}
},
"protection_onboarding": {
@@ -1331,6 +1349,11 @@
"status_pending": "Ready to install",
"status_active": "Active",
"status_revoked": "Removed",
+ "status_online": "Online",
+ "status_cooldown": "Cooldown · %{time} left",
+ "status_cooldown_short": "Cooldown",
+ "status_unprotected": "Unprotected",
+ "status_unprotected_since": "Unprotected · for %{time}",
"label_placeholder": "e.g. MacBook Pro",
"label_default": "MacBook Pro",
"label_question": "What should this Mac be called?",