3 Commits

Author SHA1 Message Date
billisdead c43b913dde fix: exclude reanimated/worklets native build, disable New Architecture
Release APK / build (push) Has been cancelled
react-native.config.js excludes react-native-reanimated and
react-native-worklets from Android native linking — neither is used in
the app and react-native-gesture-handler has a dedicated noreanimated
codepath for exactly this case.

build-apk.sh step 4.6 forces newArchEnabled=false in gradle.properties
after expo prebuild, as a fallback for New Architecture init issues
on Android 15.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 15:16:37 +02:00
billisdead 256b338ae3 fix: patch worklets and reanimated for Android 15 16KB page size support
Release APK / build (push) Has been cancelled
react-native-worklets and react-native-reanimated compile .so files
without -Wl,-z,max-page-size=16384, causing a native crash at startup
on Android 15 devices that use 16KB memory pages (Pixel 9+, Galaxy S25+).
Patch applied in build-apk.sh and CI workflow after npm install, before
the Gradle build. Guard prevents double-patching on repeated runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 14:23:38 +02:00
billisdead ec95406305 debug: add crash boundary and global error handler to surface release crashes
Release APK / build (push) Has been cancelled
Catches React render errors (ErrorBoundary) and fatal JS errors
(ErrorUtils.setGlobalHandler) and displays the stacktrace on-screen
instead of crashing to desktop. Temporary — remove after crash identified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 13:10:01 +02:00
4 changed files with 128 additions and 19 deletions
+11
View File
@@ -89,6 +89,17 @@ jobs:
[ -n "$KEYSTORE_STORE_PASSWORD" ] && echo "KEYSTORE_STORE_PASSWORD: set" || echo "KEYSTORE_STORE_PASSWORD: EMPTY"
[ -n "$KEYSTORE_KEY_PASSWORD" ] && echo "KEYSTORE_KEY_PASSWORD: set" || echo "KEYSTORE_KEY_PASSWORD: EMPTY"
- name: Patch native modules for Android 15 16KB page size
run: |
WORKLETS="node_modules/react-native-worklets/android/CMakeLists.txt"
REANIMATED="node_modules/react-native-reanimated/android/CMakeLists.txt"
if [ -f "$WORKLETS" ] && ! grep -q "max-page-size" "$WORKLETS"; then
printf '\ntarget_link_options(worklets PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$WORKLETS"
fi
if [ -f "$REANIMATED" ] && ! grep -q "max-page-size" "$REANIMATED"; then
printf '\ntarget_link_options(reanimated PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$REANIMATED"
fi
- name: Build signed APK
run: ./build-apk.sh
+78 -19
View File
@@ -1,13 +1,55 @@
import { useEffect } from "react";
import { useEffect, useState, Component } from "react";
import type { ReactNode } from "react";
import { Stack, router } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { View, Text, ScrollView } from "react-native";
import "../global.css";
import { Colors } from "@/constants/theme";
import { useSettings } from "@/hooks/useSettings";
// Catch JS errors in release mode and show them on-screen instead of
// silently crashing to desktop. Remove once the crash is identified.
class CrashBoundary extends Component<
{ children: ReactNode },
{ error: Error | null }
> {
state = { error: null };
static getDerivedStateFromError(e: Error) {
return { error: e };
}
render() {
const { error } = this.state;
if (error) {
return (
<View style={{ flex: 1, backgroundColor: "#0f1117", padding: 20, paddingTop: 60 }}>
<Text style={{ color: "#ef4444", fontSize: 16, fontWeight: "700", marginBottom: 12 }}>
CRASH copie ce texte
</Text>
<ScrollView>
<Text style={{ color: "#fff", fontSize: 11, fontFamily: "monospace" }}>
{String(error)}{"\n\n"}{(error as any).stack}
</Text>
</ScrollView>
</View>
);
}
return this.props.children;
}
}
export default function RootLayout() {
const { settings, ready } = useSettings();
const [jsError, setJsError] = useState<string | null>(null);
useEffect(() => {
const prev = (ErrorUtils as any).getGlobalHandler();
(ErrorUtils as any).setGlobalHandler((e: Error, isFatal: boolean) => {
if (isFatal) setJsError(`${String(e)}\n\n${(e as any).stack ?? ""}`);
prev?.(e, isFatal);
});
return () => (ErrorUtils as any).setGlobalHandler(prev);
}, []);
useEffect(() => {
if (!ready) return;
@@ -16,24 +58,41 @@ export default function RootLayout() {
}
}, [ready, settings.onboardingDone]);
if (jsError) {
return (
<View style={{ flex: 1, backgroundColor: "#0f1117", padding: 20, paddingTop: 60 }}>
<Text style={{ color: "#ef4444", fontSize: 16, fontWeight: "700", marginBottom: 12 }}>
FATAL JS ERROR copie ce texte
</Text>
<ScrollView>
<Text style={{ color: "#fff", fontSize: 11, fontFamily: "monospace" }}>
{jsError}
</Text>
</ScrollView>
</View>
);
}
return (
<GestureHandlerRootView style={{ flex: 1, backgroundColor: Colors.bg }}>
<StatusBar style="light" />
<Stack
screenOptions={{
headerStyle: { backgroundColor: Colors.surface },
headerTintColor: Colors.text1,
headerTitleStyle: { color: Colors.text1 },
contentStyle: { backgroundColor: Colors.bg },
animation: "slide_from_right",
}}
>
<Stack.Screen name="index" options={{ title: "SheetHappens" }} />
<Stack.Screen name="new" options={{ title: "New Incident", presentation: "modal" }} />
<Stack.Screen name="incident/[id]" options={{ title: "Incident" }} />
<Stack.Screen name="settings" options={{ title: "Settings" }} />
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
</Stack>
</GestureHandlerRootView>
<CrashBoundary>
<GestureHandlerRootView style={{ flex: 1, backgroundColor: Colors.bg }}>
<StatusBar style="light" />
<Stack
screenOptions={{
headerStyle: { backgroundColor: Colors.surface },
headerTintColor: Colors.text1,
headerTitleStyle: { color: Colors.text1 },
contentStyle: { backgroundColor: Colors.bg },
animation: "slide_from_right",
}}
>
<Stack.Screen name="index" options={{ title: "SheetHappens" }} />
<Stack.Screen name="new" options={{ title: "New Incident", presentation: "modal" }} />
<Stack.Screen name="incident/[id]" options={{ title: "Incident" }} />
<Stack.Screen name="settings" options={{ title: "Settings" }} />
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
</Stack>
</GestureHandlerRootView>
</CrashBoundary>
);
}
+26
View File
@@ -77,6 +77,32 @@ info "Running expo prebuild (Android)…"
cd "$SCRIPT_DIR"
npx expo prebuild --platform android --clean --no-install
# ─── 4.5. Patch native modules — 16KB page size (Android 15) ──────────────
info "Patching native modules for 16KB page size support (Android 15)…"
WORKLETS_CMAKE="$SCRIPT_DIR/node_modules/react-native-worklets/android/CMakeLists.txt"
REANIMATED_CMAKE="$SCRIPT_DIR/node_modules/react-native-reanimated/android/CMakeLists.txt"
if [ -f "$WORKLETS_CMAKE" ] && ! grep -q "max-page-size" "$WORKLETS_CMAKE"; then
printf '\ntarget_link_options(worklets PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$WORKLETS_CMAKE"
info "Patched react-native-worklets for 16KB pages"
fi
if [ -f "$REANIMATED_CMAKE" ] && ! grep -q "max-page-size" "$REANIMATED_CMAKE"; then
printf '\ntarget_link_options(reanimated PRIVATE "-Wl,-z,max-page-size=16384")\n' >> "$REANIMATED_CMAKE"
info "Patched react-native-reanimated for 16KB pages"
fi
# ─── 4.6. Disable New Architecture (Android 15 compat) ────────────────────
GRADLE_PROPS_NEWARCH="$SCRIPT_DIR/android/gradle.properties"
if [ -f "$GRADLE_PROPS_NEWARCH" ]; then
sed -i 's/^newArchEnabled=true/newArchEnabled=false/' "$GRADLE_PROPS_NEWARCH"
if ! grep -q "^newArchEnabled=" "$GRADLE_PROPS_NEWARCH"; then
echo "newArchEnabled=false" >> "$GRADLE_PROPS_NEWARCH"
fi
info "New Architecture disabled in gradle.properties"
fi
# ─── 5. Patch build.gradle — inject release signingConfig ──────────────────
info "Patching android/app/build.gradle for release signing…"
+13
View File
@@ -0,0 +1,13 @@
// Exclude native build for packages not used in this project.
// react-native-gesture-handler detects their absence and falls back to its
// noreanimated codepath, so GestureHandlerRootView continues to work.
module.exports = {
dependencies: {
'react-native-reanimated': {
platforms: { android: null },
},
'react-native-worklets': {
platforms: { android: null },
},
},
};