Compare commits

..

5 commits

Author SHA1 Message Date
236121a73c
chore(release): 1.12.3 2025-09-05 11:46:26 +02:00
cbd1803dc0
fix(android): battery optimisation 2025-09-05 11:46:21 +02:00
de560bd1e5
chore(release): 1.12.2 2025-08-24 12:57:31 +02:00
8487573c0f
fix: 112 2025-08-24 10:22:27 +02:00
958eee1f72
chore(ios): improve debugging 2025-08-10 13:20:58 +02:00
11 changed files with 215 additions and 67 deletions

3
.gitignore vendored
View file

@ -84,6 +84,9 @@ DerivedData
# aidigest # aidigest
codebase.md codebase.md
# Build logs
logs/
# Sensitive configuration files # Sensitive configuration files
ios/GoogleService-Info.plist ios/GoogleService-Info.plist
ios/AlerteSecours/GoogleService-Info.plist ios/AlerteSecours/GoogleService-Info.plist

View file

@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.12.3](https://github.com/alerte-secours/as-app/compare/v1.12.2...v1.12.3) (2025-09-05)
## [1.12.2](https://github.com/alerte-secours/as-app/compare/v1.12.1...v1.12.2) (2025-08-24)
### Bug Fixes
* 112 ([8487573](https://github.com/alerte-secours/as-app/commit/8487573c0f8eb6656cd5825ff217efe046d65407))
## [1.12.1](https://github.com/alerte-secours/as-app/compare/v1.12.0...v1.12.1) (2025-08-10) ## [1.12.1](https://github.com/alerte-secours/as-app/compare/v1.12.0...v1.12.1) (2025-08-10)

View file

@ -83,8 +83,8 @@ android {
applicationId 'com.alertesecours' applicationId 'com.alertesecours'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 209 versionCode 211
versionName "1.12.1" versionName "1.12.3"
multiDexEnabled true multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug') testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -2,6 +2,7 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<uses-permission android:name="android.permission.CALL_PHONE"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
@ -10,6 +11,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>

View file

@ -25,7 +25,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.12.1</string> <string>1.12.3</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -48,7 +48,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>209</string> <string>211</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>

View file

@ -1,6 +1,6 @@
{ {
"name": "alerte-secours", "name": "alerte-secours",
"version": "1.12.1", "version": "1.12.3",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "expo start --dev-client --private-key-path ./keys/private-key.pem", "start": "expo start --dev-client --private-key-path ./keys/private-key.pem",
@ -50,8 +50,8 @@
"screenshot:android": "scripts/screenshot-android.sh" "screenshot:android": "scripts/screenshot-android.sh"
}, },
"customExpoVersioning": { "customExpoVersioning": {
"versionCode": 209, "versionCode": 211,
"buildNumber": 209 "buildNumber": 211
}, },
"commit-and-tag-version": { "commit-and-tag-version": {
"scripts": { "scripts": {

View file

@ -86,6 +86,9 @@ mv ios/main.jsbundle.hbc ios/main.jsbundle
cd ios cd ios
# Create logs directory if it doesn't exist
mkdir -p ../logs
# Create archive using xcodebuild # Create archive using xcodebuild
echo "Creating archive..." echo "Creating archive..."
xcodebuild \ xcodebuild \
@ -93,6 +96,6 @@ xcodebuild \
-scheme AlerteSecours \ -scheme AlerteSecours \
-configuration Release \ -configuration Release \
-archivePath AlerteSecours.xcarchive \ -archivePath AlerteSecours.xcarchive \
archive archive 2>&1 | tee "../logs/ios-archive-$(date +%Y%m%d-%H%M%S).log"
echo "Archive completed successfully at AlerteSecours.xcarchive" echo "Archive completed successfully at AlerteSecours.xcarchive"

View file

@ -17,11 +17,13 @@ import {
import { createStyles, useTheme } from "~/theme"; import { createStyles, useTheme } from "~/theme";
import openSettings from "~/lib/native/openSettings"; import openSettings from "~/lib/native/openSettings";
import { import {
RequestDisableOptimization, requestBatteryOptimizationExemption,
BatteryOptEnabled, isBatteryOptimizationEnabled,
} from "react-native-battery-optimization-check"; openBatteryOptimizationFallbacks,
} from "~/lib/native/batteryOptimization";
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
import requestPermissionMotion from "~/permissions/requestPermissionMotion"; import requestPermissionMotion from "~/permissions/requestPermissionMotion";
import CustomButton from "~/components/CustomButton"; import CustomButton from "~/components/CustomButton";
import Text from "~/components/Text"; import Text from "~/components/Text";
@ -45,6 +47,8 @@ const HeroMode = () => {
useState(null); useState(null);
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false); const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false); const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
const [batteryOptFallbackOpened, setBatteryOptFallbackOpened] =
useState(false);
const permissions = usePermissionsState([ const permissions = usePermissionsState([
"locationBackground", "locationBackground",
"motion", "motion",
@ -75,16 +79,14 @@ const HeroMode = () => {
setBatteryOptInProgress(true); setBatteryOptInProgress(true);
// Check if battery optimization is enabled // Check if battery optimization is enabled
const isEnabled = await BatteryOptEnabled(); const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
if (isEnabled) { if (isEnabled) {
console.log( console.log("Battery optimization is enabled, requesting exemption...");
"Battery optimization is enabled, requesting to disable...",
);
// Request to disable battery optimization (opens Android Settings) // Request to disable battery optimization (opens Android Settings)
RequestDisableOptimization(); await requestBatteryOptimizationExemption();
setBatteryOptAttempted(true); setBatteryOptAttempted(true);
// Return false to indicate user needs to complete action in Settings // Return false to indicate user needs to complete action in Settings
@ -106,31 +108,45 @@ const HeroMode = () => {
const handleRequestPermissions = useCallback(async () => { const handleRequestPermissions = useCallback(async () => {
setRequesting(true); setRequesting(true);
try { try {
// Don't change step immediately to avoid race conditions
console.log("Starting permission requests..."); console.log("Starting permission requests...");
// Request battery optimization FIRST (opens Android Settings) // 1) Battery optimization (opens Settings)
// This prevents the bubbling issue by handling Settings-based permissions before in-app dialogs
const batteryOptDisabled = await handleBatteryOptimization(); const batteryOptDisabled = await handleBatteryOptimization();
console.log("Battery optimization disabled:", batteryOptDisabled); console.log("Battery optimization disabled:", batteryOptDisabled);
if (!batteryOptDisabled) {
// Settings flow opened; wait for user to return before requesting in-app permissions
setRequesting(false);
setHasAttempted(true);
return;
}
// Request motion permission second // 2) Foreground location
let fgGranted = await requestPermissionLocationForeground();
permissionsActions.setLocationForeground(fgGranted);
console.log("Location foreground permission:", fgGranted);
// 3) Background location (only after FG granted)
let bgGranted = false;
if (fgGranted) {
bgGranted = await requestPermissionLocationBackground();
permissionsActions.setLocationBackground(bgGranted);
} else {
console.log(
"Skipping background location since foreground not granted",
);
}
console.log("Location background permission:", bgGranted);
// 4) Motion
const motionGranted = await requestPermissionMotion.requestPermission(); const motionGranted = await requestPermissionMotion.requestPermission();
permissionsActions.setMotion(motionGranted); permissionsActions.setMotion(motionGranted);
console.log("Motion permission:", motionGranted); console.log("Motion permission:", motionGranted);
// Request background location last (after user returns from Settings if needed) // Step after all requests
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"); permissionWizardActions.setCurrentStep("tracking");
// Check if we should proceed to success immediately if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) {
if (locationGranted && motionGranted && batteryOptDisabled) {
permissionWizardActions.setHeroPermissionsGranted(true); permissionWizardActions.setHeroPermissionsGranted(true);
// Don't navigate immediately, let the useEffect handle it
} }
} catch (error) { } catch (error) {
console.error("Error requesting permissions:", error); console.error("Error requesting permissions:", error);
@ -143,7 +159,7 @@ const HeroMode = () => {
// Re-check battery optimization status before retrying // Re-check battery optimization status before retrying
if (Platform.OS === "android") { if (Platform.OS === "android") {
try { try {
const isEnabled = await BatteryOptEnabled(); const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
// If battery optimization is now disabled, update the store // If battery optimization is now disabled, update the store
@ -179,7 +195,7 @@ const HeroMode = () => {
const checkInitialBatteryOptimization = async () => { const checkInitialBatteryOptimization = async () => {
if (Platform.OS === "android") { if (Platform.OS === "android") {
try { try {
const isEnabled = await BatteryOptEnabled(); const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
// If already disabled, update the store // If already disabled, update the store
@ -208,7 +224,7 @@ const HeroMode = () => {
) { ) {
console.log("App became active, re-checking battery optimization..."); console.log("App became active, re-checking battery optimization...");
try { try {
const isEnabled = await BatteryOptEnabled(); const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled); setBatteryOptimizationEnabled(isEnabled);
if (!isEnabled) { if (!isEnabled) {
@ -216,6 +232,19 @@ const HeroMode = () => {
"Battery optimization disabled after returning from settings", "Battery optimization disabled after returning from settings",
); );
permissionsActions.setBatteryOptimizationDisabled(true); permissionsActions.setBatteryOptimizationDisabled(true);
} else if (!batteryOptFallbackOpened) {
try {
console.log(
"Battery optimization still enabled; opening fallback settings...",
);
await openBatteryOptimizationFallbacks();
setBatteryOptFallbackOpened(true);
} catch (e) {
console.error(
"Error opening battery optimization fallback settings:",
e,
);
}
} }
} catch (error) { } catch (error) {
console.error( console.error(
@ -234,7 +263,7 @@ const HeroMode = () => {
return () => { return () => {
subscription?.remove(); subscription?.remove();
}; };
}, [batteryOptAttempted]); }, [batteryOptAttempted, batteryOptFallbackOpened]);
useEffect(() => { useEffect(() => {
if (hasAttempted && allGranted) { if (hasAttempted && allGranted) {

View file

@ -0,0 +1,97 @@
import { Platform } from "react-native";
import SendIntentAndroid from "react-native-send-intent";
import {
RequestDisableOptimization,
BatteryOptEnabled,
} from "react-native-battery-optimization-check";
import { createLogger } from "~/lib/logger";
import { FEATURE_SCOPES } from "~/lib/logger/scopes";
const log = createLogger({
module: FEATURE_SCOPES.PERMISSIONS,
feature: "battery-optimization",
});
/**
* Returns true if battery optimization is currently ENABLED for this app on Android.
* On iOS, returns false (no battery optimization concept).
*/
export async function isBatteryOptimizationEnabled() {
if (Platform.OS !== "android") return false;
try {
const enabled = await BatteryOptEnabled();
log.info("Battery optimization status", { enabled });
return enabled;
} catch (e) {
log.error("Failed to read battery optimization status", {
error: e?.message,
stack: e?.stack,
});
// Conservative: assume enabled if unknown
return true;
}
}
/**
* Launches the primary system flow to request ignoring battery optimizations.
* This opens a Settings screen; it does not yield a synchronous result.
*
* Returns:
* - false on Android to indicate the user must complete an action in Settings
* - true on iOS (no-op)
*/
export async function requestBatteryOptimizationExemption() {
if (Platform.OS !== "android") return true;
try {
log.info("Requesting to disable battery optimization (primary intent)");
// This opens the OS dialog/settings. No result is provided, handle via AppState return.
RequestDisableOptimization();
return false;
} catch (e) {
log.error("Primary request to disable battery optimization failed", {
error: e?.message,
stack: e?.stack,
});
// Even if it throws, we'll guide users via fallbacks.
return false;
}
}
/**
* Opens best-effort fallback screens to let users disable battery optimization.
* Call this AFTER the user returns and status is still enabled.
*
* Strategy:
* - Try the list of battery optimization exceptions
* - Fallback to app settings
*/
export async function openBatteryOptimizationFallbacks() {
if (Platform.OS !== "android") return true;
// Try the generic battery optimization settings list
try {
log.info("Opening fallback: IGNORE_BATTERY_OPTIMIZATION_SETTINGS");
await SendIntentAndroid.openSettings(
"android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS",
);
return true;
} catch (e) {
log.warn("Failed to open IGNORE_BATTERY_OPTIMIZATION_SETTINGS", {
error: e?.message,
});
}
// Final fallback: app details settings
try {
log.info("Opening fallback: APPLICATION_DETAILS_SETTINGS (app details)");
await SendIntentAndroid.openAppSettings();
return true;
} catch (e) {
log.error("Failed to open APPLICATION_DETAILS_SETTINGS", {
error: e?.message,
stack: e?.stack,
});
return false;
}
}

View file

@ -2,7 +2,7 @@ import { Platform, Linking } from "react-native";
import RNImmediatePhoneCall from "react-native-immediate-phone-call"; import RNImmediatePhoneCall from "react-native-immediate-phone-call";
export function phoneCallEmergency() { export function phoneCallEmergency() {
const emergencyNumber = "+112"; const emergencyNumber = "112";
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
// Use telprompt URL scheme on iOS // Use telprompt URL scheme on iOS

View file

@ -10,9 +10,9 @@ import { Button, Title } from "react-native-paper";
import { usePermissionsState, permissionsActions } from "~/stores"; import { usePermissionsState, permissionsActions } from "~/stores";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
RequestDisableOptimization, requestBatteryOptimizationExemption,
BatteryOptEnabled, isBatteryOptimizationEnabled,
} from "react-native-battery-optimization-check"; } from "~/lib/native/batteryOptimization";
import openSettings from "~/lib/native/openSettings"; import openSettings from "~/lib/native/openSettings";
import requestPermissionFcm from "~/permissions/requestPermissionFcm"; import requestPermissionFcm from "~/permissions/requestPermissionFcm";
@ -26,23 +26,19 @@ import * as Location from "expo-location";
import * as Notifications from "expo-notifications"; import * as Notifications from "expo-notifications";
import * as Contacts from "expo-contacts"; import * as Contacts from "expo-contacts";
// Battery optimization request handler
const requestBatteryOptimizationDisable = async () => { const requestBatteryOptimizationDisable = async () => {
if (Platform.OS !== "android") { if (Platform.OS !== "android") return true;
return true; // iOS doesn't have battery optimization
}
try { try {
const isEnabled = await BatteryOptEnabled(); const enabled = await isBatteryOptimizationEnabled();
if (isEnabled) { if (enabled) {
console.log("Battery optimization enabled, requesting to disable..."); console.log("Battery optimization enabled, requesting exemption...");
RequestDisableOptimization(); await requestBatteryOptimizationExemption();
// Return false as the user needs to interact with the system dialog // User must interact in Settings; will re-check on AppState 'active'
return false; return false;
} else { }
console.log("Battery optimization already disabled"); console.log("Battery optimization already disabled");
return true; return true;
}
} catch (error) { } catch (error) {
console.error("Error handling battery optimization:", error); console.error("Error handling battery optimization:", error);
return false; return false;
@ -110,8 +106,8 @@ const checkPermissionStatus = async (permission) => {
return true; // iOS doesn't have battery optimization return true; // iOS doesn't have battery optimization
} }
try { try {
const isEnabled = await BatteryOptEnabled(); const enabled = await isBatteryOptimizationEnabled();
return !isEnabled; // Return true if optimization is disabled return !enabled; // true if optimization is disabled
} catch (error) { } catch (error) {
console.error("Error checking battery optimization:", error); console.error("Error checking battery optimization:", error);
return false; return false;
@ -200,24 +196,33 @@ export default function Permissions() {
const handleRequestPermission = async (permission) => { const handleRequestPermission = async (permission) => {
try { try {
const granted = await requestPermissions[permission](); let granted = false;
setPermissions[permission](granted);
// For battery optimization, we need to handle the async nature differently if (permission === "locationBackground") {
if ( // Ensure foreground location is granted first
permission === "batteryOptimizationDisabled" && const fgGranted = await checkPermissionStatus("locationForeground");
Platform.OS === "android" if (!fgGranted) {
) { const fgReq = await requestPermissionLocationForeground();
// Give a short delay for the system dialog to potentially complete setPermissions.locationForeground(fgReq);
setTimeout(async () => { if (!fgReq) {
const actualStatus = await checkPermissionStatus(permission); granted = false;
setPermissions[permission](actualStatus);
}, 1000);
} else { } else {
// Double-check the status to ensure UI is in sync granted = await requestPermissionLocationBackground();
}
} else {
granted = await requestPermissionLocationBackground();
}
setPermissions.locationBackground(granted);
} else {
granted = await requestPermissions[permission]();
setPermissions[permission](granted);
}
// Double-check the status to ensure UI is in sync.
// For battery optimization, this immediate check may still be 'enabled';
// we'll re-check again on AppState 'active' after returning from Settings.
const actualStatus = await checkPermissionStatus(permission); const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus); setPermissions[permission](actualStatus);
}
} catch (error) { } catch (error) {
console.error(`Error requesting ${permission} permission:`, error); console.error(`Error requesting ${permission} permission:`, error);
} }