feat(native): geräte-spezifische PNG-Icons (iphone/android/macbook/computer)

deviceImage()-Helper mappt Plattform→assets/devices/*.png; ersetzt Ionicons
in Geräte-Rows, MagicSheet und Detail-Sheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-07 22:40:25 +02:00
parent a95e66560d
commit 2c1eecd1f7
5 changed files with 59 additions and 27 deletions

View File

@ -7,6 +7,7 @@
- Onboarding now sets up the full protection using the exact same guided, gated step flow as the protection screen (single source of truth) — Android: VPN → Device Administrator → Accessibility (strict order: the tamper lock has to come last, otherwise it would block the device-admin screen); iOS: App Lock → Screen Time passcode → content filter. Previously the device-admin / screen-time hardening steps only existed in the protection screen after onboarding. - Onboarding now sets up the full protection using the exact same guided, gated step flow as the protection screen (single source of truth) — Android: VPN → Device Administrator → Accessibility (strict order: the tamper lock has to come last, otherwise it would block the device-admin screen); iOS: App Lock → Screen Time passcode → content filter. Previously the device-admin / screen-time hardening steps only existed in the protection screen after onboarding.
## Changed ## Changed
- Device list now shows device-specific icons (iPhone / Android / MacBook / PC) instead of generic outlines
- Stationary protection (Mac/Windows) now runs exclusively via Rebreak Magic — the manual offline profile download has been removed. The offline profile would have shipped the removal password in plain text inside the file (bypass risk); with Magic the lock password stays server-side and is never shown to the user. - Stationary protection (Mac/Windows) now runs exclusively via Rebreak Magic — the manual offline profile download has been removed. The offline profile would have shipped the removal password in plain text inside the file (bypass risk); with Magic the lock password stays server-side and is never shown to the user.
- Mac DNS profile hardened with `ProhibitDisablement` — the filter can no longer be toggled off in System Settings. - Mac DNS profile hardened with `ProhibitDisablement` — the filter can no longer be toggled off in System Settings.

View File

@ -1,6 +1,7 @@
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
Image,
Platform, Platform,
ScrollView, ScrollView,
Text, Text,
@ -33,21 +34,10 @@ import { AppHeader } from '../components/AppHeader';
import { MagicSheet } from '../components/devices/MagicSheet'; import { MagicSheet } from '../components/devices/MagicSheet';
import { DeviceProgressBar } from '../components/devices/DeviceProgressBar'; import { DeviceProgressBar } from '../components/devices/DeviceProgressBar';
import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet'; import { DeviceDetailSheet, type DeviceDetail } from '../components/devices/DeviceDetailSheet';
import { deviceImage } from '../components/devices/deviceIcon';
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
function mobileIcon(platform: string): React.ComponentProps<typeof Ionicons>['name'] {
if (platform === 'ios') return 'logo-apple';
if (platform === 'android') return 'logo-android';
return 'phone-portrait-outline';
}
function protectedDeviceIcon(platform: string): React.ComponentProps<typeof Ionicons>['name'] {
if (platform === 'mac') return 'laptop-outline';
if (platform === 'windows') return 'desktop-outline';
return 'globe-outline';
}
function formatLastSeen(iso: string, t: (k: string, o?: any) => string): string { function formatLastSeen(iso: string, t: (k: string, o?: any) => string): string {
const ms = Date.now() - new Date(iso).getTime(); const ms = Date.now() - new Date(iso).getTime();
const min = Math.floor(ms / 60_000); const min = Math.floor(ms / 60_000);
@ -193,7 +183,7 @@ function MobileDeviceRow({
function openDetail() { function openDetail() {
onOpenDetail({ onOpenDetail({
name: deviceName, name: deviceName,
icon: mobileIcon(device.platform), icon: deviceImage(device.platform, device.model),
platform: device.platform, platform: device.platform,
createdAt: device.createdAt, createdAt: device.createdAt,
lastSeenAt: device.lastSeenAt, lastSeenAt: device.lastSeenAt,
@ -229,7 +219,11 @@ function MobileDeviceRow({
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<Ionicons name={mobileIcon(device.platform)} size={20} color={colors.text} /> <Image
source={deviceImage(device.platform, device.model)}
style={{ width: 26, height: 26 }}
resizeMode="contain"
/>
</View> </View>
<View style={{ flex: 1, minWidth: 0 }}> <View style={{ flex: 1, minWidth: 0 }}>
@ -364,7 +358,7 @@ function ProtectedDeviceRow({
function openDetail() { function openDetail() {
onOpenDetail({ onOpenDetail({
name: device.label, name: device.label,
icon: protectedDeviceIcon(device.platform), icon: deviceImage(device.platform),
platform: device.platform, platform: device.platform,
createdAt: device.createdAt, createdAt: device.createdAt,
lastSeenAt: null, lastSeenAt: null,
@ -423,7 +417,11 @@ function ProtectedDeviceRow({
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<Ionicons name={protectedDeviceIcon(device.platform)} size={20} color={colors.text} /> <Image
source={deviceImage(device.platform)}
style={{ width: 26, height: 26 }}
resizeMode="contain"
/>
</View> </View>
<View style={{ flex: 1, minWidth: 0 }}> <View style={{ flex: 1, minWidth: 0 }}>

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Text, View } from 'react-native'; import { Image, type ImageSourcePropType, Text, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useColors } from '../../lib/theme'; import { useColors } from '../../lib/theme';
@ -14,7 +14,7 @@ const DAY_MS = 86_400_000;
export type DeviceDetail = { export type DeviceDetail = {
name: string; name: string;
icon: React.ComponentProps<typeof Ionicons>['name']; icon: ImageSourcePropType;
platform: string; platform: string;
/** ISO — Bindungs-/Verbindungsdatum */ /** ISO — Bindungs-/Verbindungsdatum */
createdAt: string; createdAt: string;
@ -106,7 +106,7 @@ export function DeviceDetailSheet({
justifyContent: 'center', justifyContent: 'center',
}} }}
> >
<Ionicons name={device.icon} size={24} color={colors.text} /> <Image source={device.icon} style={{ width: 30, height: 30 }} resizeMode="contain" />
</View> </View>
<View style={{ flex: 1, minWidth: 0 }}> <View style={{ flex: 1, minWidth: 0 }}>
<Text <Text

View File

@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Image,
Linking, Linking,
Pressable, Pressable,
Share, Share,
@ -15,6 +16,7 @@ import type { ColorScheme } from '../../lib/theme';
import { apiFetch } from '../../lib/api'; import { apiFetch } from '../../lib/api';
import { useUserPlan } from '../../hooks/useUserPlan'; import { useUserPlan } from '../../hooks/useUserPlan';
import { FormSheet } from '../FormSheet'; import { FormSheet } from '../FormSheet';
import { deviceImage } from './deviceIcon';
type PairResponse = { type PairResponse = {
code: string; code: string;
@ -360,14 +362,10 @@ export function MagicSheet({
marginRight: 12, marginRight: 12,
}} }}
> >
<Ionicons <Image
name={ source={deviceImage(undefined, d.model)}
d.model?.toLowerCase().includes('windows') style={{ width: 24, height: 24 }}
? 'desktop-outline' resizeMode="contain"
: 'laptop-outline'
}
size={20}
color={colors.text}
/> />
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>

View File

@ -0,0 +1,35 @@
import type { ImageSourcePropType } from 'react-native';
/**
* Geräte-spezifische PNG-Icons (apps/rebreak-native/assets/devices).
* React Native braucht statische require()-Pfade daher das feste Mapping.
*/
const ICONS = {
iphone: require('../../assets/devices/iphone.png'),
android: require('../../assets/devices/android.png'),
tablet: require('../../assets/devices/tablet.png'),
macbook: require('../../assets/devices/macbook.png'),
laptop: require('../../assets/devices/laptop.png'),
computer: require('../../assets/devices/computer.png'),
} as const;
/**
* Wählt das passende Geräte-PNG anhand Plattform (+ optional Modell-String).
* Fällt auf `laptop` zurück (generischer Desktop).
*/
export function deviceImage(
platform?: string | null,
model?: string | null,
): ImageSourcePropType {
const p = (platform ?? '').toLowerCase();
const m = (model ?? '').toLowerCase();
if (p.startsWith('ipad') || m.includes('ipad') || p.includes('tablet')) return ICONS.tablet;
if (p.startsWith('ios') || p.startsWith('iphone') || m.includes('iphone')) return ICONS.iphone;
if (p.startsWith('android')) return ICONS.android;
if (p.startsWith('mac') || p === 'darwin' || m.includes('macbook') || m.includes('mac')) {
return ICONS.macbook;
}
if (p.startsWith('win') || m.includes('windows')) return ICONS.computer;
return ICONS.laptop;
}