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

5
.gitignore vendored
View file

@ -84,6 +84,9 @@ DerivedData
# aidigest
codebase.md
# Build logs
logs/
# Sensitive configuration files
ios/GoogleService-Info.plist
ios/AlerteSecours/GoogleService-Info.plist
@ -96,4 +99,4 @@ android/app/google-services.json
!ios/AlerteSecours/Supporting/Expo.example.plist
!android/app/google-services.example.json
screenshot-*.png
screenshot-*.png

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.
## [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)

View file

@ -83,8 +83,8 @@ android {
applicationId 'com.alertesecours'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 209
versionName "1.12.1"
versionCode 211
versionName "1.12.3"
multiDexEnabled true
testBuildType System.getProperty('testBuildType', 'debug')
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_COARSE_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.INTERNET"/>
<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.RECEIVE_BOOT_COMPLETED"/>
<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.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>

View file

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

View file

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

View file

@ -86,6 +86,9 @@ mv ios/main.jsbundle.hbc ios/main.jsbundle
cd ios
# Create logs directory if it doesn't exist
mkdir -p ../logs
# Create archive using xcodebuild
echo "Creating archive..."
xcodebuild \
@ -93,6 +96,6 @@ xcodebuild \
-scheme AlerteSecours \
-configuration Release \
-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"

View file

@ -17,11 +17,13 @@ import {
import { createStyles, useTheme } from "~/theme";
import openSettings from "~/lib/native/openSettings";
import {
RequestDisableOptimization,
BatteryOptEnabled,
} from "react-native-battery-optimization-check";
requestBatteryOptimizationExemption,
isBatteryOptimizationEnabled,
openBatteryOptimizationFallbacks,
} from "~/lib/native/batteryOptimization";
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";
import requestPermissionMotion from "~/permissions/requestPermissionMotion";
import CustomButton from "~/components/CustomButton";
import Text from "~/components/Text";
@ -45,6 +47,8 @@ const HeroMode = () => {
useState(null);
const [batteryOptAttempted, setBatteryOptAttempted] = useState(false);
const [batteryOptInProgress, setBatteryOptInProgress] = useState(false);
const [batteryOptFallbackOpened, setBatteryOptFallbackOpened] =
useState(false);
const permissions = usePermissionsState([
"locationBackground",
"motion",
@ -75,16 +79,14 @@ const HeroMode = () => {
setBatteryOptInProgress(true);
// Check if battery optimization is enabled
const isEnabled = await BatteryOptEnabled();
const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled);
if (isEnabled) {
console.log(
"Battery optimization is enabled, requesting to disable...",
);
console.log("Battery optimization is enabled, requesting exemption...");
// Request to disable battery optimization (opens Android Settings)
RequestDisableOptimization();
await requestBatteryOptimizationExemption();
setBatteryOptAttempted(true);
// Return false to indicate user needs to complete action in Settings
@ -106,31 +108,45 @@ const HeroMode = () => {
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
// 1) Battery optimization (opens Settings)
const batteryOptDisabled = await handleBatteryOptimization();
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();
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
// Step after all requests
permissionWizardActions.setCurrentStep("tracking");
// Check if we should proceed to success immediately
if (locationGranted && motionGranted && batteryOptDisabled) {
if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) {
permissionWizardActions.setHeroPermissionsGranted(true);
// Don't navigate immediately, let the useEffect handle it
}
} catch (error) {
console.error("Error requesting permissions:", error);
@ -143,7 +159,7 @@ const HeroMode = () => {
// Re-check battery optimization status before retrying
if (Platform.OS === "android") {
try {
const isEnabled = await BatteryOptEnabled();
const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled);
// If battery optimization is now disabled, update the store
@ -179,7 +195,7 @@ const HeroMode = () => {
const checkInitialBatteryOptimization = async () => {
if (Platform.OS === "android") {
try {
const isEnabled = await BatteryOptEnabled();
const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled);
// If already disabled, update the store
@ -208,7 +224,7 @@ const HeroMode = () => {
) {
console.log("App became active, re-checking battery optimization...");
try {
const isEnabled = await BatteryOptEnabled();
const isEnabled = await isBatteryOptimizationEnabled();
setBatteryOptimizationEnabled(isEnabled);
if (!isEnabled) {
@ -216,6 +232,19 @@ const HeroMode = () => {
"Battery optimization disabled after returning from settings",
);
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) {
console.error(
@ -234,7 +263,7 @@ const HeroMode = () => {
return () => {
subscription?.remove();
};
}, [batteryOptAttempted]);
}, [batteryOptAttempted, batteryOptFallbackOpened]);
useEffect(() => {
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";
export function phoneCallEmergency() {
const emergencyNumber = "+112";
const emergencyNumber = "112";
if (Platform.OS === "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 { Ionicons } from "@expo/vector-icons";
import {
RequestDisableOptimization,
BatteryOptEnabled,
} from "react-native-battery-optimization-check";
requestBatteryOptimizationExemption,
isBatteryOptimizationEnabled,
} from "~/lib/native/batteryOptimization";
import openSettings from "~/lib/native/openSettings";
import requestPermissionFcm from "~/permissions/requestPermissionFcm";
@ -26,23 +26,19 @@ import * as Location from "expo-location";
import * as Notifications from "expo-notifications";
import * as Contacts from "expo-contacts";
// Battery optimization request handler
const requestBatteryOptimizationDisable = async () => {
if (Platform.OS !== "android") {
return true; // iOS doesn't have battery optimization
}
if (Platform.OS !== "android") return true;
try {
const isEnabled = await BatteryOptEnabled();
if (isEnabled) {
console.log("Battery optimization enabled, requesting to disable...");
RequestDisableOptimization();
// Return false as the user needs to interact with the system dialog
const enabled = await isBatteryOptimizationEnabled();
if (enabled) {
console.log("Battery optimization enabled, requesting exemption...");
await requestBatteryOptimizationExemption();
// User must interact in Settings; will re-check on AppState 'active'
return false;
} else {
console.log("Battery optimization already disabled");
return true;
}
console.log("Battery optimization already disabled");
return true;
} catch (error) {
console.error("Error handling battery optimization:", error);
return false;
@ -110,8 +106,8 @@ const checkPermissionStatus = async (permission) => {
return true; // iOS doesn't have battery optimization
}
try {
const isEnabled = await BatteryOptEnabled();
return !isEnabled; // Return true if optimization is disabled
const enabled = await isBatteryOptimizationEnabled();
return !enabled; // true if optimization is disabled
} catch (error) {
console.error("Error checking battery optimization:", error);
return false;
@ -200,24 +196,33 @@ export default function Permissions() {
const handleRequestPermission = async (permission) => {
try {
const granted = await requestPermissions[permission]();
setPermissions[permission](granted);
let granted = false;
// For battery optimization, we need to handle the async nature differently
if (
permission === "batteryOptimizationDisabled" &&
Platform.OS === "android"
) {
// Give a short delay for the system dialog to potentially complete
setTimeout(async () => {
const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus);
}, 1000);
if (permission === "locationBackground") {
// Ensure foreground location is granted first
const fgGranted = await checkPermissionStatus("locationForeground");
if (!fgGranted) {
const fgReq = await requestPermissionLocationForeground();
setPermissions.locationForeground(fgReq);
if (!fgReq) {
granted = false;
} else {
granted = await requestPermissionLocationBackground();
}
} else {
granted = await requestPermissionLocationBackground();
}
setPermissions.locationBackground(granted);
} else {
// Double-check the status to ensure UI is in sync
const actualStatus = await checkPermissionStatus(permission);
setPermissions[permission](actualStatus);
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);
setPermissions[permission](actualStatus);
} catch (error) {
console.error(`Error requesting ${permission} permission:`, error);
}