Kantongku: hardening - ErrorBoundary + catch semua load async (anti force-close)
This commit is contained in:
@@ -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,16 +84,18 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<ErrorBoundary>
|
||||||
<NavigationContainer>
|
<SafeAreaProvider>
|
||||||
<RootNavigator initialRoute={initialRoute} />
|
<NavigationContainer>
|
||||||
</NavigationContainer>
|
<RootNavigator initialRoute={initialRoute} />
|
||||||
<StatusBar
|
</NavigationContainer>
|
||||||
barStyle="light-content"
|
<StatusBar
|
||||||
translucent={false}
|
barStyle="light-content"
|
||||||
backgroundColor={colors.primary}
|
translucent={false}
|
||||||
/>
|
backgroundColor={colors.primary}
|
||||||
</SafeAreaProvider>
|
/>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
await setSetting('user_name', name.trim());
|
try {
|
||||||
await setSetting('user_gender', gender ?? 'male');
|
await setSetting('user_name', name.trim());
|
||||||
await setSetting('onboarded', '1');
|
await setSetting('user_gender', gender ?? 'male');
|
||||||
navigation.replace('Main');
|
await setSetting('onboarded', '1');
|
||||||
|
navigation.replace('Main');
|
||||||
|
} catch {
|
||||||
|
// tetap lanjut walau gagal simpan
|
||||||
|
navigation.replace('Main');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user