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:
parent
e9d34dbe78
commit
385f0b42a9
@ -6,7 +6,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|||||||
slug: "rebreak",
|
slug: "rebreak",
|
||||||
version: "0.1.0",
|
version: "0.1.0",
|
||||||
orientation: "portrait",
|
orientation: "portrait",
|
||||||
icon: "./assets/rebreak_android.png",
|
icon: "./assets/icon.png",
|
||||||
scheme: "rebreak",
|
scheme: "rebreak",
|
||||||
userInterfaceStyle: "automatic",
|
userInterfaceStyle: "automatic",
|
||||||
newArchEnabled: true,
|
newArchEnabled: true,
|
||||||
@ -38,9 +38,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
|
|||||||
package: "org.rebreak.app",
|
package: "org.rebreak.app",
|
||||||
versionCode: 2,
|
versionCode: 2,
|
||||||
adaptiveIcon: {
|
adaptiveIcon: {
|
||||||
// Play-Console-Icon (rebreak_android.png) verwenden — wird durch Adaptive-Mask
|
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
|
||||||
// gecropped (Kreis/Squircle), evtl. Edges leicht beschnitten je nach Design.
|
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem
|
||||||
foregroundImage: "./assets/rebreak_android.png",
|
// 1024er-Canvas mit ~21% Padding. Hintergrund = Marken-Dunkel.
|
||||||
|
foregroundImage: "./assets/adaptive-foreground.png",
|
||||||
backgroundColor: "#0a0a0a",
|
backgroundColor: "#0a0a0a",
|
||||||
},
|
},
|
||||||
permissions: [
|
permissions: [
|
||||||
|
|||||||
BIN
apps/rebreak-native/assets/adaptive-foreground.png
Normal file
BIN
apps/rebreak-native/assets/adaptive-foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useRouter, type RelativePathString } from 'expo-router';
|
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="h-14 flex-row items-center justify-between px-5">
|
||||||
<View className="flex-row items-center" style={{ gap: 8 }}>
|
<View className="flex-row items-center" style={{ gap: 8 }}>
|
||||||
{showBack ? (
|
{showBack ? (
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
style={({ pressed }) => ({
|
activeOpacity={0.6}
|
||||||
opacity: pressed ? 0.6 : 1,
|
style={{
|
||||||
marginLeft: -8,
|
marginLeft: -8,
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
})}
|
}}
|
||||||
accessibilityLabel="Zurück"
|
accessibilityLabel="Zurück"
|
||||||
>
|
>
|
||||||
<Ionicons name="chevron-back" size={22} color={colors.text} />
|
<Ionicons name="chevron-back" size={22} color={colors.text} />
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
) : null}
|
) : null}
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 18, color: colors.text, letterSpacing: -0.3 }}>
|
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 18, color: colors.text, letterSpacing: -0.3 }}>
|
||||||
{title ?? t('appHeader.appName')}
|
{title ?? t('appHeader.appName')}
|
||||||
@ -82,18 +82,18 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex-row items-center gap-1">
|
<View className="flex-row items-center gap-1">
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={() => setNotifOpen(true)}
|
onPress={() => setNotifOpen(true)}
|
||||||
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
||||||
style={({ pressed }) => ({
|
activeOpacity={0.7}
|
||||||
opacity: pressed ? 0.7 : 1,
|
style={{
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="notifications-outline" size={22} color={colors.textMuted} />
|
<Ionicons name="notifications-outline" size={22} color={colors.textMuted} />
|
||||||
{badge > 0 && (
|
{badge > 0 && (
|
||||||
@ -103,14 +103,17 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Avatar = Trigger für Dropdown-Menu (kein separates 3-Punkte-Icon) */}
|
{/* Avatar = Trigger für Dropdown-Menu (kein separates 3-Punkte-Icon).
|
||||||
<Pressable
|
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)}
|
onPress={() => setMenuOpen(true)}
|
||||||
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
||||||
style={({ pressed }) => ({
|
activeOpacity={0.7}
|
||||||
opacity: pressed ? 0.7 : 1,
|
style={{
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
@ -118,7 +121,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: showAvatarImage ? colors.surfaceElevated : colors.brandOrange,
|
backgroundColor: showAvatarImage ? colors.surfaceElevated : colors.brandOrange,
|
||||||
})}
|
}}
|
||||||
>
|
>
|
||||||
{showAvatarImage ? (
|
{showAvatarImage ? (
|
||||||
<Image
|
<Image
|
||||||
@ -129,7 +132,7 @@ export function AppHeader({ notifCount, showBack, title }: Props = {}) {
|
|||||||
) : (
|
) : (
|
||||||
<Text className="text-white text-xs" style={{ fontFamily: 'Nunito_700Bold' }}>{initials}</Text>
|
<Text className="text-white text-xs" style={{ fontFamily: 'Nunito_700Bold' }}>{initials}</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<HeaderDropdownMenu
|
<HeaderDropdownMenu
|
||||||
visible={menuOpen}
|
visible={menuOpen}
|
||||||
|
|||||||
@ -193,12 +193,27 @@ export const protection = {
|
|||||||
* Phase-Berechnung folgt der State-Machine im Plan.
|
* Phase-Berechnung folgt der State-Machine im Plan.
|
||||||
*/
|
*/
|
||||||
async getCombinedState(): Promise<ProtectionState> {
|
async getCombinedState(): Promise<ProtectionState> {
|
||||||
const [layers, cooldown, backend] = await Promise.all([
|
const [rawLayers, cooldown, backend] = await Promise.all([
|
||||||
this.getDeviceState(),
|
this.getDeviceState(),
|
||||||
this.getCooldownStatus(),
|
this.getCooldownStatus(),
|
||||||
this.getBackendProtectionState(),
|
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 allLayersOn = isAllLayersOn(layers);
|
||||||
const iosLockActive =
|
const iosLockActive =
|
||||||
layers.appDeletionLock ?? layers.familyControls ?? false;
|
layers.appDeletionLock ?? layers.familyControls ?? false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user