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",
|
||||
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: [
|
||||
|
||||
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 { 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}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user