import React, { useState, useCallback, useEffect } from "react"; import { View, StyleSheet, Image, ScrollView, Platform, AppState, } from "react-native"; import { Title } from "react-native-paper"; import { Ionicons, Entypo } from "@expo/vector-icons"; import { permissionsActions, usePermissionsState, permissionWizardActions, } from "~/stores"; import { createStyles, useTheme } from "~/theme"; import openSettings from "~/lib/native/openSettings"; import { RequestDisableOptimization, BatteryOptEnabled, } from "react-native-battery-optimization-check"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; import requestPermissionMotion from "~/permissions/requestPermissionMotion"; import CustomButton from "~/components/CustomButton"; import Text from "~/components/Text"; const skipMessages = [ "Non merci, je préfère rester égoïste", "Les héros ? Très peu pour moi !", "J'ai peur des responsabilités...", "Je suis trop douillet pour être un héros...", "Non merci, je préfère rester sur mon canapé", "Les héros ? Ça me donne des boutons !", "J'ai une allergie aux bonnes actions", "Désolé, mon chat a besoin de moi", ]; const HeroMode = () => { const [requesting, setRequesting] = useState(false); const [hasAttempted, setHasAttempted] = useState(false); const [hasRetried, setHasRetried] = useState(false); const [batteryOptimizationEnabled, setBatteryOptimizationEnabled] = useState(null); const [batteryOptAttempted, setBatteryOptAttempted] = useState(false); const [batteryOptInProgress, setBatteryOptInProgress] = useState(false); const permissions = usePermissionsState([ "locationBackground", "motion", "batteryOptimizationDisabled", ]); const theme = useTheme(); const [skipMessage] = useState(() => { const randomIndex = Math.floor(Math.random() * skipMessages.length); return skipMessages[randomIndex]; }); const handleNext = useCallback(() => { permissionWizardActions.setCurrentStep("success"); }, []); const handleSkip = useCallback(() => { permissionWizardActions.setCurrentStep("skipInfo"); }, []); const handleBatteryOptimization = useCallback(async () => { if (Platform.OS !== "android") { permissionsActions.setBatteryOptimizationDisabled(true); return true; } try { setBatteryOptInProgress(true); // Check if battery optimization is enabled const isEnabled = await BatteryOptEnabled(); setBatteryOptimizationEnabled(isEnabled); if (isEnabled) { console.log( "Battery optimization is enabled, requesting to disable...", ); // Request to disable battery optimization (opens Android Settings) RequestDisableOptimization(); setBatteryOptAttempted(true); // Return false to indicate user needs to complete action in Settings return false; } else { console.log("Battery optimization already disabled"); permissionsActions.setBatteryOptimizationDisabled(true); return true; } } catch (error) { console.error("Error handling battery optimization:", error); setBatteryOptAttempted(true); return false; } finally { setBatteryOptInProgress(false); } }, []); const handleRequestPermissions = useCallback(async () => { setRequesting(true); try { // Don't change step immediately to avoid race conditions console.log("Starting permission requests..."); // Request battery optimization FIRST (opens Android Settings) // This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs const batteryOptDisabled = await handleBatteryOptimization(); console.log("Battery optimization disabled:", batteryOptDisabled); // Request motion permission second const motionGranted = await requestPermissionMotion.requestPermission(); permissionsActions.setMotion(motionGranted); console.log("Motion permission:", motionGranted); // Request background location last (after user returns from Settings if needed) const locationGranted = await requestPermissionLocationBackground(); permissionsActions.setLocationBackground(locationGranted); console.log("Location background permission:", locationGranted); // Only set step to tracking after all permission requests are complete permissionWizardActions.setCurrentStep("tracking"); // Check if we should proceed to success immediately if (locationGranted && motionGranted && batteryOptDisabled) { permissionWizardActions.setHeroPermissionsGranted(true); // Don't navigate immediately, let the useEffect handle it } } catch (error) { console.error("Error requesting permissions:", error); } setRequesting(false); setHasAttempted(true); }, [handleBatteryOptimization]); const handleRetry = useCallback(async () => { // Re-check battery optimization status before retrying if (Platform.OS === "android") { try { const isEnabled = await BatteryOptEnabled(); setBatteryOptimizationEnabled(isEnabled); // If battery optimization is now disabled, update the store if (!isEnabled) { console.log("Battery optimization now disabled after retry"); permissionsActions.setBatteryOptimizationDisabled(true); } } catch (error) { console.error("Error re-checking battery optimization:", error); } } // Only request permissions again if some are still missing const needsRetry = !permissions.locationBackground || !permissions.motion || (Platform.OS === "android" && batteryOptimizationEnabled); if (needsRetry) { await handleRequestPermissions(); } setHasRetried(true); }, [handleRequestPermissions, permissions, batteryOptimizationEnabled]); const allGranted = permissions.locationBackground && permissions.motion && (Platform.OS === "ios" || !batteryOptimizationEnabled); // Check battery optimization status on mount useEffect(() => { const checkInitialBatteryOptimization = async () => { if (Platform.OS === "android") { try { const isEnabled = await BatteryOptEnabled(); setBatteryOptimizationEnabled(isEnabled); // If already disabled, update the store if (!isEnabled) { permissionsActions.setBatteryOptimizationDisabled(true); } } catch (error) { console.error("Error checking initial battery optimization:", error); } } else { // iOS doesn't have battery optimization, so mark as disabled permissionsActions.setBatteryOptimizationDisabled(true); } }; checkInitialBatteryOptimization(); }, []); // Listen for app state changes to re-check battery optimization when user returns from settings useEffect(() => { const handleAppStateChange = async (nextAppState) => { if ( nextAppState === "active" && Platform.OS === "android" && batteryOptAttempted ) { console.log("App became active, re-checking battery optimization..."); try { const isEnabled = await BatteryOptEnabled(); setBatteryOptimizationEnabled(isEnabled); if (!isEnabled) { console.log( "Battery optimization disabled after returning from settings", ); permissionsActions.setBatteryOptimizationDisabled(true); } } catch (error) { console.error( "Error re-checking battery optimization on app focus:", error, ); } } }; const subscription = AppState.addEventListener( "change", handleAppStateChange, ); return () => { subscription?.remove(); }; }, [batteryOptAttempted]); useEffect(() => { if (hasAttempted && allGranted) { handleNext(); } }, [hasAttempted, allGranted, handleNext]); const styles = useStyles(); const renderWarnings = () => { const warnings = []; if (!permissions.motion) { warnings.push( "Sans la détection de mouvement, la localisation en arrière-plan ne pourra pas fonctionner.", ); } if (!permissions.locationBackground) { warnings.push( "Sans la localisation en arrière-plan, vous ne pourrez pas être alerté des situations d'urgence à proximité lorsque l'application est fermée.", ); } // Battery optimization warning is now handled in the Android settings box return warnings.length > 0 ? ( {warnings.map((warning, index) => ( {warning} ))} ) : null; }; const renderAndroidPermissionWarning = () => { const hasBatteryOptimizationIssue = batteryOptimizationEnabled && batteryOptAttempted; return ( Paramètres Android {hasBatteryOptimizationIssue && ( L'optimisation de la batterie est encore activée. L'application pourrait ne pas fonctionner correctement en arrière-plan. )} Sur Android, les permissions peuvent être automatiquement révoquées si l'application n'est pas utilisée pendant une longue période. Pour garantir le bon fonctionnement de l'application : 1. Accédez aux paramètres de l'application 2. Recherchez la section "Autorisations" ou "Permissions" 3. Désactivez l'option "Supprimer les autorisations si l'application n'est pas utilisée" (l'emplacement exact peut varier selon votre version d'Android) {hasBatteryOptimizationIssue && ( Pour désactiver l'optimisation de la batterie : 4. Recherchez "Batterie" ou "Optimisation de la batterie" 5. Trouvez cette application dans la liste 6. Sélectionnez "Ne pas optimiser" ou "Désactiver l'optimisation" )} Ouvrir les paramètres ); }; const renderPlatformWarning = () => { if (Platform.OS === "ios") { return renderIOSPermissionWarning(); } else if (Platform.OS === "android") { return renderAndroidPermissionWarning(); } return null; }; const renderIOSPermissionWarning = () => { return ( Paramètres iOS Pour garantir le bon fonctionnement de l'application en arrière-plan, quelques réglages supplémentaires sont nécessaires. 1. Activez l'actualisation en arrière-plan : • {"Réglages > Général > Actualisation en arrière-plan"} • Activez l'option pour cette application 2. Attention aux modes qui peuvent limiter le fonctionnement : • Le mode économie d'énergie • Le mode concentration (Ne pas déranger) Ouvrir les réglages ); }; const renderButton = () => { if (!hasAttempted) { return ( {batteryOptInProgress ? "Traitement de l'optimisation de la batterie..." : "J'accorde les permissions"} ); } if (allGranted) { return ( Suivant ); } return ( <> {skipMessage} {batteryOptInProgress ? "Vérification en cours..." : "Réessayer d'accorder les permissions"} {hasRetried && ( <> Si les permissions ne sont pas accordées, vous devez les activer manuellement dans les paramètres de votre téléphone. Paramètres )} ); }; return ( Rejoignez les vrais{"\n"} <Text style={styles.subtitle}>Soyez prêt à agir</Text> Pas besoin de super-pouvoirs pour être un héros, il vous suffit simplement d'activer les autorisations nécessaires qui permettront de vous alerter. Ensuite, répondre présent pour apporter votre aide fera la différence ! Permissions requises Localisation en arrière-plan : pour être alerté des situations d'urgence à proximité même lorsque l'application est fermée. Détection de mouvement : pour optimiser la consommation de batterie lors de la localisation en arrière-plan, aucune donnée de mouvement n'est stockée ni transmise. {Platform.OS === "android" && ( Optimisation de la batterie : désactiver l'optimisation de la batterie pour cette application afin qu'elle puisse fonctionner correctement en arrière-plan. )} Important N'oubliez pas de garder la localisation de votre téléphone activée pour que l'application puisse fonctionner correctement ! {!allGranted && hasAttempted && renderWarnings()} {renderPlatformWarning()} {renderButton()} ); }; const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({ container: { flex: 1, }, scrollView: { flex: 1, }, content: { padding: 20, }, heroHeader: { alignItems: "center", marginBottom: 30, }, heroImage: { width: 120, height: 120, marginBottom: 10, }, title: { fontSize: 28, fontWeight: "bold", textAlign: "center", marginBottom: 20, }, subtitle: { fontSize: 22, }, description: { fontSize: 16, marginBottom: 30, lineHeight: 24, textAlign: "left", color: colors.onSurfaceVariant, }, section: { marginBottom: 25, }, sectionTitle: { fontSize: 18, fontWeight: "bold", color: colors.primary, marginBottom: 15, }, permissionList: { gap: 15, }, permissionItem: { flexDirection: "row", alignItems: "flex-start", }, permissionContent: { flex: 1, }, icon: { marginRight: 10, marginTop: 2, color: colors.primary, }, permissionText: { fontSize: 16, flex: 1, lineHeight: 22, textAlign: "left", color: colors.onSurfaceVariant, }, warningsContainer: { backgroundColor: colors.surfaceVariant, padding: 15, borderRadius: 8, marginBottom: 20, borderWidth: 1, borderColor: colors.error, }, warning: { fontSize: 15, lineHeight: 20, textAlign: "left", color: colors.onSurfaceVariant, }, settingsHint: { fontSize: 14, lineHeight: 20, textAlign: "center", marginBottom: 10, fontStyle: "italic", color: colors.onSurfaceVariant, }, buttonContainer: { marginTop: 20, gap: 10, }, // Android styles androidWarning: { backgroundColor: colors.surfaceVariant, padding: 20, borderRadius: 8, marginBottom: 20, borderWidth: 1, borderColor: colors.warn, }, androidWarningCritical: { borderColor: colors.error, borderWidth: 2, }, androidWarningHeader: { flexDirection: "row", alignItems: "center", marginBottom: 15, gap: 10, }, androidWarningTitle: { fontSize: 18, fontWeight: "bold", color: colors.warn, }, androidWarningDescription: { fontSize: 16, lineHeight: 22, color: colors.onSurfaceVariant, marginBottom: 15, }, androidWarningSteps: { marginBottom: 15, }, androidWarningText: { fontSize: 16, lineHeight: 22, color: colors.onSurfaceVariant, marginBottom: 10, }, androidWarningStep: { fontSize: 16, lineHeight: 22, color: colors.onSurfaceVariant, marginLeft: 15, marginBottom: 5, }, androidSettingsButton: { marginTop: 5, color: colors.primary, }, batteryOptimizationAlert: { backgroundColor: colors.errorContainer || colors.surfaceVariant, padding: 15, borderRadius: 6, marginBottom: 15, borderWidth: 1, borderColor: colors.error, }, batteryOptimizationAlertText: { fontSize: 15, lineHeight: 20, fontWeight: "500", }, batteryOptimizationText: { fontWeight: "600", color: colors.error, }, // iOS styles iosWarning: { backgroundColor: colors.surfaceVariant, padding: 20, borderRadius: 8, marginBottom: 20, borderWidth: 1, borderColor: colors.warn, }, iosWarningHeader: { flexDirection: "row", alignItems: "center", marginBottom: 15, gap: 10, }, iosWarningTitle: { fontSize: 18, fontWeight: "bold", color: colors.warn, }, iosWarningDescription: { fontSize: 16, lineHeight: 22, color: colors.onSurfaceVariant, marginBottom: 15, }, iosWarningSteps: { marginBottom: 15, }, iosWarningText: { fontSize: 16, lineHeight: 22, color: colors.onSurfaceVariant, marginBottom: 10, }, iosWarningStep: { fontSize: 16, lineHeight: 22, color: colors.onSurfaceVariant, marginLeft: 15, marginBottom: 5, }, iosSettingsButton: { marginTop: 5, color: colors.primary, }, })); export default HeroMode;