fix(android): blocker toggles + invisible avatar + adaptive icon

- protection.ts: normalize Android device-state keys (vpn/accessibility/
  tamperLock) to the iOS-shaped names the UI reads (urlFilter/familyControls/
  appDeletionLock) — on Android the layers came back under different keys, so
  blocker.tsx saw all toggles as undefined → always off → optimistic toggle
  flipped back to off after enabling
- AppHeader.tsx: avatar/bell/back Pressable-with-style-fn → TouchableOpacity
  with plain style — style-fn was swallowing width/height on Android → 0×0
  + overflow:hidden → avatar invisible (same pattern as Mac-CTA fix 7d04e42)
- app.config.ts: adaptiveIcon.foregroundImage → padded adaptive-foreground.png
  (logo in ~66% safe zone, was full-bleed → clipped by launcher mask);
  icon → icon.png (clean 1024 opaque, was the 512px alpha variant)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-11 14:52:42 +02:00
parent e9d34dbe78
commit 385f0b42a9
4 changed files with 41 additions and 22 deletions

View File

@ -6,7 +6,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
slug: "rebreak",
version: "0.1.0",
orientation: "portrait",
icon: "./assets/rebreak_android.png",
icon: "./assets/icon.png",
scheme: "rebreak",
userInterfaceStyle: "automatic",
newArchEnabled: true,
@ -38,9 +38,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
package: "org.rebreak.app",
versionCode: 2,
adaptiveIcon: {
// Play-Console-Icon (rebreak_android.png) verwenden — wird durch Adaptive-Mask
// gecropped (Kreis/Squircle), evtl. Edges leicht beschnitten je nach Design.
foregroundImage: "./assets/rebreak_android.png",
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem
// 1024er-Canvas mit ~21% Padding. Hintergrund = Marken-Dunkel.
foregroundImage: "./assets/adaptive-foreground.png",
backgroundColor: "#0a0a0a",
},
permissions: [

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { View, Text, Pressable, Image } from 'react-native';
import { View, Text, TouchableOpacity, Image } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useRouter, type RelativePathString } from 'expo-router';
@ -59,22 +59,22 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
<View className="h-14 flex-row items-center justify-between px-5">
<View className="flex-row items-center" style={{ gap: 8 }}>
{showBack ? (
<Pressable
<TouchableOpacity
onPress={() => router.back()}
hitSlop={10}
style={({ pressed }) => ({
opacity: pressed ? 0.6 : 1,
activeOpacity={0.6}
style={{
marginLeft: -8,
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
})}
}}
accessibilityLabel="Zurück"
>
<Ionicons name="chevron-back" size={22} color={colors.text} />
</Pressable>
</TouchableOpacity>
) : null}
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 18, color: colors.text, letterSpacing: -0.3 }}>
{title ?? t('appHeader.appName')}
@ -82,18 +82,18 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
</View>
<View className="flex-row items-center gap-1">
<Pressable
<TouchableOpacity
onPress={() => setNotifOpen(true)}
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
style={({ pressed }) => ({
opacity: pressed ? 0.7 : 1,
activeOpacity={0.7}
style={{
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.surface,
alignItems: 'center',
justifyContent: 'center',
})}
}}
>
<Ionicons name="notifications-outline" size={22} color={colors.textMuted} />
{badge > 0 && (
@ -103,14 +103,17 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
</Text>
</View>
)}
</Pressable>
</TouchableOpacity>
{/* Avatar = Trigger für Dropdown-Menu (kein separates 3-Punkte-Icon) */}
<Pressable
{/* Avatar = Trigger für Dropdown-Menu (kein separates 3-Punkte-Icon).
TouchableOpacity statt Pressable-mit-style-fn Pressable's style-fn
schluckt auf Android manchmal width/height 0×0 + overflow:hidden
Avatar unsichtbar (vgl. Mac-CTA-Fix 7d04e42). */}
<TouchableOpacity
onPress={() => setMenuOpen(true)}
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
style={({ pressed }) => ({
opacity: pressed ? 0.7 : 1,
activeOpacity={0.7}
style={{
width: 36,
height: 36,
borderRadius: 18,
@ -118,7 +121,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
justifyContent: 'center',
overflow: 'hidden',
backgroundColor: showAvatarImage ? colors.surfaceElevated : colors.brandOrange,
})}
}}
>
{showAvatarImage ? (
<Image
@ -129,7 +132,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
) : (
<Text className="text-white text-xs" style={{ fontFamily: 'Nunito_700Bold' }}>{initials}</Text>
)}
</Pressable>
</TouchableOpacity>
<HeaderDropdownMenu
visible={menuOpen}

View File

@ -193,12 +193,27 @@ export const protection = {
* Phase-Berechnung folgt der State-Machine im Plan.
*/
async getCombinedState(): Promise<ProtectionState> {
const [layers, cooldown, backend] = await Promise.all([
const [rawLayers, cooldown, backend] = await Promise.all([
this.getDeviceState(),
this.getCooldownStatus(),
this.getBackendProtectionState(),
]);
// Android's native module reports {vpn, accessibility, tamperLock}; the UI
// (blocker.tsx, isAllLayersOn) reads the iOS-shaped names {urlFilter,
// familyControls, appDeletionLock}. Alias them so consumers are platform-
// agnostic. Android "App-Lock" = AccessibilityService + armed tamper-lock,
// so the lock-state maps to `tamperLock`.
const layers: DeviceLayers =
Platform.OS === "android" && rawLayers.urlFilter === undefined
? ({
...rawLayers,
urlFilter: rawLayers.vpn,
familyControls: rawLayers.tamperLock,
appDeletionLock: rawLayers.tamperLock,
} as DeviceLayers)
: rawLayers;
const allLayersOn = isAllLayersOn(layers);
const iosLockActive =
layers.appDeletionLock ?? layers.familyControls ?? false;