Kantongku: hardening - ErrorBoundary + catch semua load async (anti force-close)

This commit is contained in:
Bro
2026-08-16 21:20:39 +07:00
parent 639f3a4ef6
commit cb22c1394a
6 changed files with 50 additions and 29 deletions
+25 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { Component, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import { ActivityIndicator, StatusBar, StyleSheet, Text, View } from 'react-native'; import { ActivityIndicator, StatusBar, StyleSheet, Text, View } from 'react-native';
import * as SplashScreen from 'expo-splash-screen'; import * as SplashScreen from 'expo-splash-screen';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
@@ -11,6 +12,27 @@ import { colors } from './src/theme/colors';
SplashScreen.preventAutoHideAsync().catch(() => {}); SplashScreen.preventAutoHideAsync().catch(() => {});
/** Tangkap error render agar tidak force-close: tampilkan layar aman. */
class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean; message: string }> {
state = { hasError: false, message: '' };
static getDerivedStateFromError(e: unknown) {
return { hasError: true, message: e instanceof Error ? e.message : String(e) };
}
render() {
if (this.state.hasError) {
return (
<View style={styles.center}>
<Text style={styles.errorTitle}>Terjadi kesalahan 😢</Text>
<Text style={styles.errorText}>{this.state.message}</Text>
</View>
);
}
return this.props.children;
}
}
export default function App() { export default function App() {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -62,6 +84,7 @@ export default function App() {
} }
return ( return (
<ErrorBoundary>
<SafeAreaProvider> <SafeAreaProvider>
<NavigationContainer> <NavigationContainer>
<RootNavigator initialRoute={initialRoute} /> <RootNavigator initialRoute={initialRoute} />
@@ -72,6 +95,7 @@ export default function App() {
backgroundColor={colors.primary} backgroundColor={colors.primary}
/> />
</SafeAreaProvider> </SafeAreaProvider>
</ErrorBoundary>
); );
} }
+1 -3
View File
@@ -116,9 +116,7 @@ export function DashboardScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
let active = true; let active = true;
load().then(() => { load().catch(() => {});
if (!active) return;
});
return () => { return () => {
active = false; active = false;
}; };
+1 -3
View File
@@ -60,9 +60,7 @@ export function HistoryScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
let active = true; let active = true;
load().then(() => { load().catch(() => {});
if (!active) return;
});
return () => { return () => {
active = false; active = false;
}; };
+5
View File
@@ -27,10 +27,15 @@ export function OnboardingScreen() {
const [gender, setGender] = useState<'male' | 'female' | null>(null); const [gender, setGender] = useState<'male' | 'female' | null>(null);
const handleFinish = async () => { const handleFinish = async () => {
try {
await setSetting('user_name', name.trim()); await setSetting('user_name', name.trim());
await setSetting('user_gender', gender ?? 'male'); await setSetting('user_gender', gender ?? 'male');
await setSetting('onboarded', '1'); await setSetting('onboarded', '1');
navigation.replace('Main'); navigation.replace('Main');
} catch {
// tetap lanjut walau gagal simpan
navigation.replace('Main');
}
}; };
return ( return (
+1 -3
View File
@@ -69,9 +69,7 @@ export function SavingsScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
let active = true; let active = true;
load().then(() => { load().catch(() => {});
if (!active) return;
});
return () => { return () => {
active = false; active = false;
}; };
+3 -5
View File
@@ -102,15 +102,13 @@ export function SettingsScreen() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
let active = true; let active = true;
loadSettings().then(() => { loadSettings().catch(() => {});
if (!active) return;
});
getRecurring().then((r) => { getRecurring().then((r) => {
if (active) setRecurring(r); if (active) setRecurring(r);
}); }).catch(() => {});
getCategories().then((c) => { getCategories().then((c) => {
if (active) setCategories(c); if (active) setCategories(c);
}); }).catch(() => {});
return () => { return () => {
active = false; active = false;
}; };