Compare commits
No commits in common. "main" and "v1.11.15" have entirely different histories.
33 changed files with 286 additions and 905 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -84,9 +84,6 @@ 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
|
||||||
|
@ -99,4 +96,4 @@ android/app/google-services.json
|
||||||
!ios/AlerteSecours/Supporting/Expo.example.plist
|
!ios/AlerteSecours/Supporting/Expo.example.plist
|
||||||
!android/app/google-services.example.json
|
!android/app/google-services.example.json
|
||||||
|
|
||||||
screenshot-*.png
|
screenshot-*.png
|
37
CHANGELOG.md
37
CHANGELOG.md
|
@ -2,43 +2,6 @@
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* menu index typo ([8d8da91](https://github.com/alerte-secours/as-app/commit/8d8da916965c1dd7feaa8b011ea854591c859e03))
|
|
||||||
* placeholder in dark theme for chat input ([d5ad23d](https://github.com/alerte-secours/as-app/commit/d5ad23d1dae521e4a99f757505adec3f23a7914c))
|
|
||||||
* **push-notif:** label "undefined à " ([ef9b503](https://github.com/alerte-secours/as-app/commit/ef9b5037fbb97fa597194760a9d3a22a04eeeeda))
|
|
||||||
* **theming:** alertes archivées buttons ([d6a3e94](https://github.com/alerte-secours/as-app/commit/d6a3e94ea710a494623a8636ccad71205094d9c6))
|
|
||||||
* typo ([0b5e936](https://github.com/alerte-secours/as-app/commit/0b5e936714054fe8647b8520d6432ab29fe2ecb7))
|
|
||||||
* **voice-message:** wip ([28b8b3d](https://github.com/alerte-secours/as-app/commit/28b8b3d826685de053ae5ff7f7931b24a64920b4))
|
|
||||||
|
|
||||||
## [1.12.0](https://github.com/alerte-secours/as-app/compare/v1.11.17...v1.12.0) (2025-08-02)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **heartbeat:** remove ([69d9fc9](https://github.com/alerte-secours/as-app/commit/69d9fc9a6a1383bbbf5f1b5f5641d01b2378168b))
|
|
||||||
|
|
||||||
## [1.11.17](https://github.com/alerte-secours/as-app/compare/v1.11.16...v1.11.17) (2025-07-30)
|
|
||||||
|
|
||||||
## [1.11.16](https://github.com/alerte-secours/as-app/compare/v1.11.15...v1.11.16) (2025-07-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **io:** headless ([6ea01c0](https://github.com/alerte-secours/as-app/commit/6ea01c0c6d7f3cb8dbff51138b20f3fbba5b9766))
|
|
||||||
|
|
||||||
## [1.11.15](https://github.com/alerte-secours/as-app/compare/v1.11.14...v1.11.15) (2025-07-26)
|
## [1.11.15](https://github.com/alerte-secours/as-app/compare/v1.11.14...v1.11.15) (2025-07-26)
|
||||||
|
|
||||||
## [1.11.14](https://github.com/alerte-secours/as-app/compare/v1.11.13...v1.11.14) (2025-07-25)
|
## [1.11.14](https://github.com/alerte-secours/as-app/compare/v1.11.13...v1.11.14) (2025-07-25)
|
||||||
|
|
|
@ -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 211
|
versionCode 205
|
||||||
versionName "1.12.3"
|
versionName "1.11.15"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<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"/>
|
||||||
|
@ -11,7 +10,6 @@
|
||||||
<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"/>
|
||||||
|
|
40
index.js
40
index.js
|
@ -20,7 +20,7 @@ import { onBackgroundEvent as notificationBackgroundEvent } from "~/notification
|
||||||
import onMessageReceived from "~/notifications/onMessageReceived";
|
import onMessageReceived from "~/notifications/onMessageReceived";
|
||||||
|
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
// import { executeHeartbeatSync } from "~/location/backgroundTask";
|
import { executeHeartbeatSync } from "~/location/backgroundTask";
|
||||||
|
|
||||||
// setup notification, this have to stay in index.js
|
// setup notification, this have to stay in index.js
|
||||||
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
notifee.onBackgroundEvent(notificationBackgroundEvent);
|
||||||
|
@ -36,23 +36,23 @@ const geolocBgLogger = createLogger({
|
||||||
task: "headless",
|
task: "headless",
|
||||||
});
|
});
|
||||||
|
|
||||||
// const HeadlessTask = async (event) => {
|
const HeadlessTask = async (event) => {
|
||||||
// try {
|
try {
|
||||||
// switch (event?.name) {
|
switch (event?.name) {
|
||||||
// case "heartbeat":
|
case "heartbeat":
|
||||||
// await executeHeartbeatSync();
|
await executeHeartbeatSync();
|
||||||
// break;
|
break;
|
||||||
// default:
|
default:
|
||||||
// break;
|
break;
|
||||||
// }
|
}
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// geolocBgLogger.error("HeadlessTask error", {
|
geolocBgLogger.error("HeadlessTask error", {
|
||||||
// error,
|
error,
|
||||||
// event,
|
event,
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
|
|
||||||
// if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
// BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
BackgroundGeolocation.registerHeadlessTask(HeadlessTask);
|
||||||
// }
|
}
|
||||||
|
|
|
@ -163,12 +163,7 @@
|
||||||
8EC12A68941D40E98E0D60BE /* Fix Xcode 15 Bug */,
|
8EC12A68941D40E98E0D60BE /* Fix Xcode 15 Bug */,
|
||||||
49AEAB1D332B45ED9A37B009 /* Fix Xcode 15 Bug */,
|
49AEAB1D332B45ED9A37B009 /* Fix Xcode 15 Bug */,
|
||||||
D75A41050AB3445786799848 /* Fix Xcode 15 Bug */,
|
D75A41050AB3445786799848 /* Fix Xcode 15 Bug */,
|
||||||
FB7FA195D27D412AA897F419 /* Fix Xcode 15 Bug */,
|
ABC6C5A0D48A4B7980D60E1B /* Remove signature files (Xcode workaround) */,
|
||||||
0C44FF6DBD8F4BDD8D2B9784 /* Fix Xcode 15 Bug */,
|
|
||||||
AC008438EEF4422BA1C35CDF /* Fix Xcode 15 Bug */,
|
|
||||||
1A6C945D28C14747A29A3560 /* Fix Xcode 15 Bug */,
|
|
||||||
1C287A64431A4C0A859F067B /* Fix Xcode 15 Bug */,
|
|
||||||
EBD8BAB94522461484E3792D /* Remove signature files (Xcode workaround) */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
|
@ -581,176 +576,6 @@ fi";
|
||||||
shellScript = "
|
shellScript = "
|
||||||
echo \"Remove signature files (Xcode workaround)\";
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
";
|
|
||||||
};
|
|
||||||
FB7FA195D27D412AA897F419 /* Fix Xcode 15 Bug */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Fix Xcode 15 Bug";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
|
||||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
|
||||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
|
||||||
fi";
|
|
||||||
};
|
|
||||||
B1FDDB484A8E497F9FF7F32C /* Remove signature files (Xcode workaround) */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Remove signature files (Xcode workaround)";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "
|
|
||||||
echo \"Remove signature files (Xcode workaround)\";
|
|
||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
|
||||||
";
|
|
||||||
};
|
|
||||||
0C44FF6DBD8F4BDD8D2B9784 /* Fix Xcode 15 Bug */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Fix Xcode 15 Bug";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
|
||||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
|
||||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
|
||||||
fi";
|
|
||||||
};
|
|
||||||
658BC0C976C44270ACBDF3C6 /* Remove signature files (Xcode workaround) */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Remove signature files (Xcode workaround)";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "
|
|
||||||
echo \"Remove signature files (Xcode workaround)\";
|
|
||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
|
||||||
";
|
|
||||||
};
|
|
||||||
AC008438EEF4422BA1C35CDF /* Fix Xcode 15 Bug */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Fix Xcode 15 Bug";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
|
||||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
|
||||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
|
||||||
fi";
|
|
||||||
};
|
|
||||||
6743177E81F94D198E926A21 /* Remove signature files (Xcode workaround) */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Remove signature files (Xcode workaround)";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "
|
|
||||||
echo \"Remove signature files (Xcode workaround)\";
|
|
||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
|
||||||
";
|
|
||||||
};
|
|
||||||
1A6C945D28C14747A29A3560 /* Fix Xcode 15 Bug */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Fix Xcode 15 Bug";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
|
||||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
|
||||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
|
||||||
fi";
|
|
||||||
};
|
|
||||||
96170835D29D4D569C60B051 /* Remove signature files (Xcode workaround) */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Remove signature files (Xcode workaround)";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "
|
|
||||||
echo \"Remove signature files (Xcode workaround)\";
|
|
||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
|
||||||
";
|
|
||||||
};
|
|
||||||
1C287A64431A4C0A859F067B /* Fix Xcode 15 Bug */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Fix Xcode 15 Bug";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "if [ \"$XCODE_VERSION_MAJOR\" = \"1500\" ]; then
|
|
||||||
echo \"Remove signature files (Xcode 15 workaround)\"
|
|
||||||
find \"$BUILD_DIR/${CONFIGURATION}-iphoneos\" -name \"*.signature\" -type f | xargs -r rm
|
|
||||||
fi";
|
|
||||||
};
|
|
||||||
EBD8BAB94522461484E3792D /* Remove signature files (Xcode workaround) */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
name = "Remove signature files (Xcode workaround)";
|
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "
|
|
||||||
echo \"Remove signature files (Xcode workaround)\";
|
|
||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
|
||||||
";
|
";
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
|
@ -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.3</string>
|
<string>1.11.15</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>211</string>
|
<string>205</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "alerte-secours",
|
"name": "alerte-secours",
|
||||||
"version": "1.12.3",
|
"version": "1.11.15",
|
||||||
"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": 211,
|
"versionCode": 205,
|
||||||
"buildNumber": 211
|
"buildNumber": 205
|
||||||
},
|
},
|
||||||
"commit-and-tag-version": {
|
"commit-and-tag-version": {
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -86,9 +86,6 @@ 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 \
|
||||||
|
@ -96,6 +93,6 @@ xcodebuild \
|
||||||
-scheme AlerteSecours \
|
-scheme AlerteSecours \
|
||||||
-configuration Release \
|
-configuration Release \
|
||||||
-archivePath AlerteSecours.xcarchive \
|
-archivePath AlerteSecours.xcarchive \
|
||||||
archive 2>&1 | tee "../logs/ios-archive-$(date +%Y%m%d-%H%M%S).log"
|
archive
|
||||||
|
|
||||||
echo "Archive completed successfully at AlerteSecours.xcarchive"
|
echo "Archive completed successfully at AlerteSecours.xcarchive"
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { useUpdates } from "~/updates";
|
||||||
import Error from "~/components/Error";
|
import Error from "~/components/Error";
|
||||||
|
|
||||||
import useTrackLocation from "~/hooks/useTrackLocation";
|
import useTrackLocation from "~/hooks/useTrackLocation";
|
||||||
// import { initializeBackgroundFetch } from "~/services/backgroundFetch";
|
import { initializeBackgroundFetch } from "~/services/backgroundFetch";
|
||||||
import useMount from "~/hooks/useMount";
|
import useMount from "~/hooks/useMount";
|
||||||
|
|
||||||
const appLogger = createLogger({
|
const appLogger = createLogger({
|
||||||
|
@ -221,22 +221,22 @@ function AppContent() {
|
||||||
useNetworkListener();
|
useNetworkListener();
|
||||||
useTrackLocation();
|
useTrackLocation();
|
||||||
|
|
||||||
// useMount(() => {
|
useMount(() => {
|
||||||
// const setupBackgroundFetch = async () => {
|
const setupBackgroundFetch = async () => {
|
||||||
// try {
|
try {
|
||||||
// appLogger.info("Setting up BackgroundFetch");
|
appLogger.info("Setting up BackgroundFetch");
|
||||||
// await initializeBackgroundFetch();
|
await initializeBackgroundFetch();
|
||||||
// appLogger.debug("BackgroundFetch setup completed");
|
appLogger.debug("BackgroundFetch setup completed");
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// lifecycleLogger.error("BackgroundFetch setup failed", {
|
lifecycleLogger.error("BackgroundFetch setup failed", {
|
||||||
// error: error?.message,
|
error: error?.message,
|
||||||
// });
|
});
|
||||||
// errorHandler(error);
|
errorHandler(error);
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
|
|
||||||
// setupBackgroundFetch();
|
setupBackgroundFetch();
|
||||||
// });
|
});
|
||||||
|
|
||||||
// Handle deep links after app is initialized with error handling
|
// Handle deep links after app is initialized with error handling
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default React.memo(function TextArea({
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
showSoftInputOnFocus={keyboardEnabled} // controlled by state
|
showSoftInputOnFocus={keyboardEnabled} // controlled by state
|
||||||
placeholder="Message"
|
placeholder="Message"
|
||||||
placeholderTextColor={colors.placeholder}
|
placeholderTextColor={colors.onBackgroundDisabled}
|
||||||
onTouchStart={() => {
|
onTouchStart={() => {
|
||||||
if (!keyboardEnabled) {
|
if (!keyboardEnabled) {
|
||||||
setKeyboardEnabled(true);
|
setKeyboardEnabled(true);
|
||||||
|
|
|
@ -38,16 +38,16 @@ const recordingSettings = {
|
||||||
outputFormat: AndroidOutputFormat.MPEG_4,
|
outputFormat: AndroidOutputFormat.MPEG_4,
|
||||||
audioEncoder: AndroidAudioEncoder.AAC,
|
audioEncoder: AndroidAudioEncoder.AAC,
|
||||||
sampleRate: 44100,
|
sampleRate: 44100,
|
||||||
numberOfChannels: 1,
|
numberOfChannels: 2,
|
||||||
bitRate: 64000,
|
bitRate: 128000,
|
||||||
},
|
},
|
||||||
ios: {
|
ios: {
|
||||||
extension: ".m4a",
|
extension: ".m4a",
|
||||||
outputFormat: IOSOutputFormat.MPEG4AAC,
|
outputFormat: IOSOutputFormat.MPEG4AAC,
|
||||||
audioQuality: IOSAudioQuality.MAX,
|
audioQuality: IOSAudioQuality.MAX,
|
||||||
sampleRate: 44100,
|
sampleRate: 44100,
|
||||||
numberOfChannels: 1,
|
numberOfChannels: 2,
|
||||||
bitRate: 64000,
|
bitRate: 128000,
|
||||||
linearPCMBitDepth: 16,
|
linearPCMBitDepth: 16,
|
||||||
linearPCMIsBigEndian: false,
|
linearPCMIsBigEndian: false,
|
||||||
linearPCMIsFloat: false,
|
linearPCMIsFloat: false,
|
||||||
|
@ -194,7 +194,7 @@ export default React.memo(function ChatInput({
|
||||||
staysActiveInBackground: true,
|
staysActiveInBackground: true,
|
||||||
});
|
});
|
||||||
const { sound: _sound } = await recording.createNewLoadedSoundAsync({
|
const { sound: _sound } = await recording.createNewLoadedSoundAsync({
|
||||||
isLooping: false,
|
isLooping: true,
|
||||||
isMuted: false,
|
isMuted: false,
|
||||||
volume: 1.0,
|
volume: 1.0,
|
||||||
rate: 1.0,
|
rate: 1.0,
|
||||||
|
@ -205,12 +205,13 @@ export default React.memo(function ChatInput({
|
||||||
|
|
||||||
const uploadAudio = useCallback(async () => {
|
const uploadAudio = useCallback(async () => {
|
||||||
const uri = recording.getURI();
|
const uri = recording.getURI();
|
||||||
|
const filetype = uri.split(".").pop();
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("data[alertId]", alertId);
|
fd.append("data[alertId]", alertId);
|
||||||
fd.append("data[file]", {
|
fd.append("data[file]", {
|
||||||
uri,
|
uri,
|
||||||
type: "audio/mp4",
|
type: `audio/${filetype}`,
|
||||||
name: "audioRecord.m4a",
|
name: "audioRecord",
|
||||||
});
|
});
|
||||||
await network.oaFilesKy.post("audio/upload", {
|
await network.oaFilesKy.post("audio/upload", {
|
||||||
body: fd,
|
body: fd,
|
||||||
|
|
|
@ -24,13 +24,7 @@ export default function MessageRow({
|
||||||
const { contentType, text, audioFileUuid, userId, createdAt, username } = row;
|
const { contentType, text, audioFileUuid, userId, createdAt, username } = row;
|
||||||
|
|
||||||
const audioFileUri =
|
const audioFileUri =
|
||||||
contentType === "audio"
|
contentType === "audio" ? `${env.MINIO_URL}/audio/${audioFileUuid}` : null;
|
||||||
? `${env.MINIO_URL}/audio/${audioFileUuid}.m4a`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// if (contentType === "audio" && __DEV__) {
|
|
||||||
// console.log(`[MessageRow] Audio URL: ${audioFileUri}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const isMine = userId === sessionUserId;
|
const isMine = userId === sessionUserId;
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,11 @@ 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 {
|
||||||
requestBatteryOptimizationExemption,
|
RequestDisableOptimization,
|
||||||
isBatteryOptimizationEnabled,
|
BatteryOptEnabled,
|
||||||
openBatteryOptimizationFallbacks,
|
} from "react-native-battery-optimization-check";
|
||||||
} 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";
|
||||||
|
@ -47,8 +45,6 @@ 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",
|
||||||
|
@ -79,14 +75,16 @@ const HeroMode = () => {
|
||||||
setBatteryOptInProgress(true);
|
setBatteryOptInProgress(true);
|
||||||
|
|
||||||
// Check if battery optimization is enabled
|
// Check if battery optimization is enabled
|
||||||
const isEnabled = await isBatteryOptimizationEnabled();
|
const isEnabled = await BatteryOptEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
console.log("Battery optimization is enabled, requesting exemption...");
|
console.log(
|
||||||
|
"Battery optimization is enabled, requesting to disable...",
|
||||||
|
);
|
||||||
|
|
||||||
// Request to disable battery optimization (opens Android Settings)
|
// Request to disable battery optimization (opens Android Settings)
|
||||||
await requestBatteryOptimizationExemption();
|
RequestDisableOptimization();
|
||||||
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
|
||||||
|
@ -108,45 +106,31 @@ 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...");
|
||||||
|
|
||||||
// 1) Battery optimization (opens Settings)
|
// 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();
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Foreground location
|
// Request motion permission second
|
||||||
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);
|
||||||
|
|
||||||
// Step after all requests
|
// 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");
|
permissionWizardActions.setCurrentStep("tracking");
|
||||||
|
|
||||||
if (fgGranted && bgGranted && motionGranted && batteryOptDisabled) {
|
// Check if we should proceed to success immediately
|
||||||
|
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);
|
||||||
|
@ -159,7 +143,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 isBatteryOptimizationEnabled();
|
const isEnabled = await BatteryOptEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
// If battery optimization is now disabled, update the store
|
// If battery optimization is now disabled, update the store
|
||||||
|
@ -195,7 +179,7 @@ const HeroMode = () => {
|
||||||
const checkInitialBatteryOptimization = async () => {
|
const checkInitialBatteryOptimization = async () => {
|
||||||
if (Platform.OS === "android") {
|
if (Platform.OS === "android") {
|
||||||
try {
|
try {
|
||||||
const isEnabled = await isBatteryOptimizationEnabled();
|
const isEnabled = await BatteryOptEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
// If already disabled, update the store
|
// If already disabled, update the store
|
||||||
|
@ -224,7 +208,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 isBatteryOptimizationEnabled();
|
const isEnabled = await BatteryOptEnabled();
|
||||||
setBatteryOptimizationEnabled(isEnabled);
|
setBatteryOptimizationEnabled(isEnabled);
|
||||||
|
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
|
@ -232,19 +216,6 @@ 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(
|
||||||
|
@ -263,7 +234,7 @@ const HeroMode = () => {
|
||||||
return () => {
|
return () => {
|
||||||
subscription?.remove();
|
subscription?.remove();
|
||||||
};
|
};
|
||||||
}, [batteryOptAttempted, batteryOptFallbackOpened]);
|
}, [batteryOptAttempted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAttempted && allGranted) {
|
if (hasAttempted && allGranted) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as Location from "expo-location";
|
import * as Location from "expo-location";
|
||||||
import { useState, useRef, useEffect, useCallback } from "react";
|
import { useState, useRef, useEffect, useCallback } from "react";
|
||||||
import { storeLocation, getStoredLocation } from "~/location/storage";
|
import { storeLocation, getStoredLocation } from "~/utils/location/storage";
|
||||||
import { createLogger } from "~/lib/logger";
|
import { createLogger } from "~/lib/logger";
|
||||||
import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes";
|
import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,6 @@ class AudioSlider extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
mapAudioToCurrentTime = async () => {
|
mapAudioToCurrentTime = async () => {
|
||||||
if (!this.soundObject) return;
|
|
||||||
await this.soundObject.setPositionAsync(this.state.currentTime);
|
await this.soundObject.setPositionAsync(this.state.currentTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -123,7 +122,6 @@ class AudioSlider extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
play = async () => {
|
play = async () => {
|
||||||
if (!this.soundObject) return;
|
|
||||||
if (this.registry && this.pauseAllBeforePlay) {
|
if (this.registry && this.pauseAllBeforePlay) {
|
||||||
const players = this.registry.getAll();
|
const players = this.registry.getAll();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -136,14 +134,12 @@ class AudioSlider extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
pause = async () => {
|
pause = async () => {
|
||||||
if (!this.soundObject) return;
|
|
||||||
await this.soundObject.pauseAsync();
|
await this.soundObject.pauseAsync();
|
||||||
this.setState({ playing: false }); // This is for the play-button to go to pause
|
this.setState({ playing: false }); // This is for the play-button to go to pause
|
||||||
Animated.timing(this.state.dotOffset, { useNativeDriver: false }).stop(); // Will also call animationPausedOrStopped()
|
Animated.timing(this.state.dotOffset, { useNativeDriver: false }).stop(); // Will also call animationPausedOrStopped()
|
||||||
};
|
};
|
||||||
|
|
||||||
startMovingDot = async () => {
|
startMovingDot = async () => {
|
||||||
if (!this.soundObject) return;
|
|
||||||
const status = await this.soundObject.getStatusAsync();
|
const status = await this.soundObject.getStatusAsync();
|
||||||
const durationLeft = status["durationMillis"] - status["positionMillis"];
|
const durationLeft = status["durationMillis"] - status["positionMillis"];
|
||||||
|
|
||||||
|
@ -160,7 +156,6 @@ class AudioSlider extends PureComponent {
|
||||||
// Audio has been paused
|
// Audio has been paused
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.soundObject) return;
|
|
||||||
// Animation-duration is over (reset Animation and Audio):
|
// Animation-duration is over (reset Animation and Audio):
|
||||||
await sleep(200); // In case animation has finished, but audio has not
|
await sleep(200); // In case animation has finished, but audio has not
|
||||||
this.setState({ playing: false });
|
this.setState({ playing: false });
|
||||||
|
@ -169,16 +164,6 @@ class AudioSlider extends PureComponent {
|
||||||
await this.soundObject.setPositionAsync(0);
|
await this.soundObject.setPositionAsync(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePlaybackFinished = async () => {
|
|
||||||
// console.log(`[AudioSlider] Playback finished, resetting for replay`);
|
|
||||||
// Reset for replay instead of unloading
|
|
||||||
this.setState({ playing: false });
|
|
||||||
await this.state.dotOffset.setValue({ x: 0, y: 0 });
|
|
||||||
if (this.soundObject) {
|
|
||||||
await this.soundObject.stopAsync();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
measureTrack = (event) => {
|
measureTrack = (event) => {
|
||||||
this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height}
|
this.setState({ trackLayout: event.nativeEvent.layout }); // {x, y, width, height}
|
||||||
};
|
};
|
||||||
|
@ -186,92 +171,27 @@ class AudioSlider extends PureComponent {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
// https://github.com/olapiv/expo-audio-player/issues/13
|
// https://github.com/olapiv/expo-audio-player/issues/13
|
||||||
|
|
||||||
const audioUrl = this.props.audio;
|
|
||||||
|
|
||||||
const loadAudio = async () => {
|
const loadAudio = async () => {
|
||||||
const tryLoad = async (ext) => {
|
|
||||||
// console.log(`[AudioSlider] Attempting to load with extension: ${ext}`);
|
|
||||||
const { sound } = await Audio.Sound.createAsync({
|
|
||||||
uri: audioUrl,
|
|
||||||
overrideFileExtensionAndroid: ext,
|
|
||||||
});
|
|
||||||
return sound;
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastError = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First try with m4a (preferred)
|
const { sound: newSound } = await Audio.Sound.createAsync({
|
||||||
const sound = await tryLoad("m4a");
|
uri: this.props.audio,
|
||||||
// console.log(`[AudioSlider] Successfully loaded with m4a extension`);
|
});
|
||||||
this.soundObject = sound;
|
this.soundObject = newSound;
|
||||||
await this.soundObject.setIsLoopingAsync(false);
|
|
||||||
|
// // https://github.com/expo/expo/issues/1873
|
||||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
||||||
if (!status.didJustFinish) return;
|
if (!status.didJustFinish) return;
|
||||||
this.handlePlaybackFinished();
|
this.soundObject.unloadAsync().catch(() => {});
|
||||||
});
|
});
|
||||||
return;
|
} catch (error) {
|
||||||
} catch (err1) {
|
console.log("Error loading audio:", error);
|
||||||
// console.log(`[AudioSlider] Failed to load with m4a:`, err1.message);
|
|
||||||
lastError = err1;
|
|
||||||
try {
|
|
||||||
// Fallback to mp4
|
|
||||||
const sound = await tryLoad("mp4");
|
|
||||||
// console.log(`[AudioSlider] Successfully loaded with mp4 extension`);
|
|
||||||
this.soundObject = sound;
|
|
||||||
await this.soundObject.setIsLoopingAsync(false);
|
|
||||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
|
||||||
if (!status.didJustFinish) return;
|
|
||||||
this.handlePlaybackFinished();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} catch (err2) {
|
|
||||||
// console.log(`[AudioSlider] Failed to load with mp4:`, err2.message);
|
|
||||||
lastError = err2;
|
|
||||||
try {
|
|
||||||
// Last fallback to aac
|
|
||||||
const sound = await tryLoad("aac");
|
|
||||||
// console.log(`[AudioSlider] Successfully loaded with aac extension`);
|
|
||||||
this.soundObject = sound;
|
|
||||||
await this.soundObject.setIsLoopingAsync(false);
|
|
||||||
this.soundObject.setOnPlaybackStatusUpdate((status) => {
|
|
||||||
if (!status.didJustFinish) return;
|
|
||||||
this.handlePlaybackFinished();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} catch (err3) {
|
|
||||||
// console.log(`[AudioSlider] Failed to load with aac:`, err3.message);
|
|
||||||
lastError = err3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All attempts failed
|
|
||||||
console.error(
|
|
||||||
`[AudioSlider] All load attempts failed for ${audioUrl}. Last error:`,
|
|
||||||
lastError,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await loadAudio();
|
await loadAudio();
|
||||||
|
|
||||||
if (!this.soundObject) {
|
const status = await this.soundObject.getStatusAsync();
|
||||||
// Loading failed; avoid further calls and leave UI inert or show error
|
this.setState({ duration: status.durationMillis });
|
||||||
console.log(
|
|
||||||
`[AudioSlider] No sound object created, setting duration to 0`,
|
|
||||||
);
|
|
||||||
this.setState({ duration: 0 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const status = await this.soundObject.getStatusAsync();
|
|
||||||
this.setState({ duration: status.durationMillis });
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error getting audio status:", error);
|
|
||||||
this.setState({ duration: 0 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This requires measureTrack to have been called.
|
// This requires measureTrack to have been called.
|
||||||
this.state.dotOffset.addListener(() => {
|
this.state.dotOffset.addListener(() => {
|
||||||
|
@ -287,9 +207,7 @@ class AudioSlider extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentWillUnmount() {
|
async componentWillUnmount() {
|
||||||
if (this.soundObject) {
|
await this.soundObject.unloadAsync();
|
||||||
await this.soundObject.unloadAsync();
|
|
||||||
}
|
|
||||||
this.state.dotOffset.removeAllListeners();
|
this.state.dotOffset.removeAllListeners();
|
||||||
if (this.registry) {
|
if (this.registry) {
|
||||||
this.registry.unregister(this);
|
this.registry.unregister(this);
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
122
src/location/backgroundTask.js
Normal file
122
src/location/backgroundTask.js
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import BackgroundGeolocation from "react-native-background-geolocation";
|
||||||
|
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
|
||||||
|
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
|
||||||
|
// Constants for persistence
|
||||||
|
// const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
|
||||||
|
const FORCE_SYNC_INTERVAL = 1 * 60 * 1000; // DEBUGGING
|
||||||
|
|
||||||
|
const geolocBgLogger = createLogger({
|
||||||
|
service: "background-task",
|
||||||
|
task: "headless",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions for persisting sync time
|
||||||
|
const getLastSyncTime = async () => {
|
||||||
|
try {
|
||||||
|
const value = await memoryAsyncStorage.getItem(
|
||||||
|
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
||||||
|
);
|
||||||
|
return value ? parseInt(value, 10) : Date.now();
|
||||||
|
} catch (error) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLastSyncTime = async (time) => {
|
||||||
|
try {
|
||||||
|
await memoryAsyncStorage.setItem(
|
||||||
|
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
||||||
|
time.toString(),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// silent error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shared heartbeat logic - mutualized between Android and iOS
|
||||||
|
const executeSync = async () => {
|
||||||
|
let syncPerformed = false;
|
||||||
|
let syncSuccessful = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
syncPerformed = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Change pace to ensure location updates
|
||||||
|
await BackgroundGeolocation.changePace(true);
|
||||||
|
|
||||||
|
// Perform sync
|
||||||
|
await BackgroundGeolocation.sync();
|
||||||
|
|
||||||
|
syncSuccessful = true;
|
||||||
|
} catch (syncError) {
|
||||||
|
syncSuccessful = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result information for BackgroundFetch
|
||||||
|
return {
|
||||||
|
syncPerformed,
|
||||||
|
syncSuccessful,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Return error result for BackgroundFetch
|
||||||
|
return {
|
||||||
|
syncPerformed,
|
||||||
|
syncSuccessful: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const executeHeartbeatSync = async () => {
|
||||||
|
const lastSyncTime = await getLastSyncTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastSync = now - lastSyncTime;
|
||||||
|
|
||||||
|
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
||||||
|
geolocBgLogger.info("Forcing location sync", {
|
||||||
|
timeSinceLastSync,
|
||||||
|
forceInterval: FORCE_SYNC_INTERVAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const syncResult = await Promise.race([
|
||||||
|
executeSync(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("changePace timeout")), 10000),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await setLastSyncTime(now);
|
||||||
|
|
||||||
|
geolocBgLogger.info("Force sync completed successfully", {
|
||||||
|
syncResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
|
} catch (syncError) {
|
||||||
|
geolocBgLogger.error("Force sync failed", {
|
||||||
|
error: syncError.message,
|
||||||
|
timeSinceLastSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncPerformed: true,
|
||||||
|
syncSuccessful: false,
|
||||||
|
error: syncError.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
geolocBgLogger.debug("Sync not needed yet", {
|
||||||
|
timeSinceLastSync,
|
||||||
|
forceInterval: FORCE_SYNC_INTERVAL,
|
||||||
|
timeUntilNextSync: FORCE_SYNC_INTERVAL - timeSinceLastSync,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
syncPerformed: false,
|
||||||
|
syncSuccessful: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,296 +0,0 @@
|
||||||
import { Platform } from "react-native";
|
|
||||||
import BackgroundGeolocation from "react-native-background-geolocation";
|
|
||||||
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
|
|
||||||
import { STORAGE_KEYS } from "~/storage/storageKeys";
|
|
||||||
import { createLogger } from "~/lib/logger";
|
|
||||||
import { getStoredLocation } from "./storage";
|
|
||||||
import { getAuthState } from "~/stores";
|
|
||||||
import env from "~/env";
|
|
||||||
|
|
||||||
// Constants for persistence
|
|
||||||
const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
|
|
||||||
// const FORCE_SYNC_INTERVAL = 1 * 60 * 1000; // DEBUGGING
|
|
||||||
|
|
||||||
const geolocBgLogger = createLogger({
|
|
||||||
service: "background-task",
|
|
||||||
task: "headless",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper functions for persisting sync time
|
|
||||||
const getLastSyncTime = async () => {
|
|
||||||
try {
|
|
||||||
const value = await memoryAsyncStorage.getItem(
|
|
||||||
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
|
||||||
);
|
|
||||||
return value ? parseInt(value, 10) : Date.now();
|
|
||||||
} catch (error) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLastSyncTime = async (time) => {
|
|
||||||
try {
|
|
||||||
await memoryAsyncStorage.setItem(
|
|
||||||
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
|
|
||||||
time.toString(),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
// silent error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeSyncAndroid = async () => {
|
|
||||||
await BackgroundGeolocation.changePace(true);
|
|
||||||
await BackgroundGeolocation.sync();
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeSyncIOS = async () => {
|
|
||||||
const debugWebhook =
|
|
||||||
"https://webhook.site/433b6aca-b169-4073-924a-4f089ca30406";
|
|
||||||
|
|
||||||
// Helper function to send debug info
|
|
||||||
const sendDebug = async (step, data = {}) => {
|
|
||||||
try {
|
|
||||||
// Build query string manually since URLSearchParams is not available in React Native
|
|
||||||
const queryData = {
|
|
||||||
step,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
...Object.entries(data).reduce((acc, [key, value]) => {
|
|
||||||
acc[key] =
|
|
||||||
typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryString = Object.entries(queryData)
|
|
||||||
.map(
|
|
||||||
([key, value]) =>
|
|
||||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
|
|
||||||
)
|
|
||||||
.join("&");
|
|
||||||
|
|
||||||
await fetch(`${debugWebhook}?${queryString}`, { method: "GET" });
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore debug errors
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Debug point 1: Function start
|
|
||||||
await sendDebug("1_function_start", { platform: "iOS" });
|
|
||||||
|
|
||||||
// Debug point 2: Before getStoredLocation
|
|
||||||
await sendDebug("2_before_get_stored_location");
|
|
||||||
|
|
||||||
const locationData = await getStoredLocation();
|
|
||||||
|
|
||||||
// Debug point 3: After getStoredLocation
|
|
||||||
await sendDebug("3_after_get_stored_location", {
|
|
||||||
hasData: !!locationData,
|
|
||||||
timestamp: locationData?.timestamp || "null",
|
|
||||||
hasCoords: !!locationData?.coords,
|
|
||||||
latitude: locationData?.coords?.latitude || "null",
|
|
||||||
longitude: locationData?.coords?.longitude || "null",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!locationData) {
|
|
||||||
geolocBgLogger.debug("No stored location data found, skipping sync");
|
|
||||||
await sendDebug("3a_no_location_data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { timestamp, coords } = locationData;
|
|
||||||
|
|
||||||
// Check if timestamp is too old (> 2 weeks)
|
|
||||||
const now = new Date();
|
|
||||||
const locationTime = new Date(timestamp);
|
|
||||||
const twoWeeksInMs = 14 * 24 * 60 * 60 * 1000; // 2 weeks in milliseconds
|
|
||||||
const locationAge = now - locationTime;
|
|
||||||
|
|
||||||
// Debug point 4: Timestamp validation
|
|
||||||
await sendDebug("4_timestamp_validation", {
|
|
||||||
locationAge: locationAge,
|
|
||||||
maxAge: twoWeeksInMs,
|
|
||||||
isTooOld: locationAge > twoWeeksInMs,
|
|
||||||
timestamp: timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (locationAge > twoWeeksInMs) {
|
|
||||||
geolocBgLogger.debug("Stored location is too old, skipping sync", {
|
|
||||||
locationAge: locationAge,
|
|
||||||
maxAge: twoWeeksInMs,
|
|
||||||
timestamp: timestamp,
|
|
||||||
});
|
|
||||||
await sendDebug("4a_location_too_old");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get auth token
|
|
||||||
const { userToken } = getAuthState();
|
|
||||||
|
|
||||||
// Debug point 5: Auth token check
|
|
||||||
await sendDebug("5_auth_token_check", {
|
|
||||||
hasToken: !!userToken,
|
|
||||||
tokenLength: userToken ? userToken.length : 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userToken) {
|
|
||||||
geolocBgLogger.debug("No auth token available, skipping sync");
|
|
||||||
await sendDebug("5a_no_auth_token");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate coordinates
|
|
||||||
if (
|
|
||||||
!coords ||
|
|
||||||
typeof coords.latitude !== "number" ||
|
|
||||||
typeof coords.longitude !== "number"
|
|
||||||
) {
|
|
||||||
geolocBgLogger.error("Invalid coordinates in stored location", {
|
|
||||||
coords,
|
|
||||||
});
|
|
||||||
await sendDebug("5b_invalid_coordinates", {
|
|
||||||
hasCoords: !!coords,
|
|
||||||
latType: typeof coords?.latitude,
|
|
||||||
lonType: typeof coords?.longitude,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare payload according to API spec
|
|
||||||
const payload = {
|
|
||||||
location: {
|
|
||||||
event: "heartbeat",
|
|
||||||
coords: {
|
|
||||||
latitude: coords.latitude,
|
|
||||||
longitude: coords.longitude,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
geolocBgLogger.debug("Syncing location to server", {
|
|
||||||
url: env.GEOLOC_SYNC_URL,
|
|
||||||
coords: payload.location.coords,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug point 6: Before sync request
|
|
||||||
await sendDebug("6_before_sync_request", {
|
|
||||||
url: env.GEOLOC_SYNC_URL,
|
|
||||||
latitude: payload.location.coords.latitude,
|
|
||||||
longitude: payload.location.coords.longitude,
|
|
||||||
event: payload.location.event,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make HTTP request
|
|
||||||
const response = await fetch(env.GEOLOC_SYNC_URL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${userToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
await sendDebug("7a_sync_http_error", {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
});
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
|
||||||
|
|
||||||
if (responseData.ok !== true) {
|
|
||||||
await sendDebug("7b_sync_api_error", {
|
|
||||||
apiOk: responseData.ok,
|
|
||||||
responseData: JSON.stringify(responseData),
|
|
||||||
});
|
|
||||||
throw new Error(`API returned ok: ${responseData.ok}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug point 7: Sync success
|
|
||||||
await sendDebug("7_sync_success", {
|
|
||||||
status: response.status,
|
|
||||||
latitude: payload.location.coords.latitude,
|
|
||||||
longitude: payload.location.coords.longitude,
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.info("iOS location sync completed successfully", {
|
|
||||||
status: response.status,
|
|
||||||
coords: payload.location.coords,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// Debug point 8: Error catch
|
|
||||||
await sendDebug("8_error_caught", {
|
|
||||||
errorMessage: error.message,
|
|
||||||
errorName: error.name,
|
|
||||||
errorStack: error.stack ? error.stack.substring(0, 500) : "no_stack",
|
|
||||||
});
|
|
||||||
|
|
||||||
geolocBgLogger.error("iOS location sync failed", {
|
|
||||||
error: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shared heartbeat logic - mutualized between Android and iOS
|
|
||||||
const executeSync = async () => {
|
|
||||||
if (Platform.OS === "ios") {
|
|
||||||
await executeSyncIOS();
|
|
||||||
} else if (Platform.OS === "android") {
|
|
||||||
await executeSyncAndroid();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const executeHeartbeatSync = async () => {
|
|
||||||
const lastSyncTime = await getLastSyncTime();
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastSync = now - lastSyncTime;
|
|
||||||
|
|
||||||
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
|
|
||||||
geolocBgLogger.info("Forcing location sync", {
|
|
||||||
timeSinceLastSync,
|
|
||||||
forceInterval: FORCE_SYNC_INTERVAL,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const syncResult = await Promise.race([
|
|
||||||
executeSync(),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("changePace timeout")), 20000),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await setLastSyncTime(now);
|
|
||||||
|
|
||||||
geolocBgLogger.info("Force sync completed successfully", {
|
|
||||||
syncResult,
|
|
||||||
});
|
|
||||||
|
|
||||||
return syncResult;
|
|
||||||
} catch (syncError) {
|
|
||||||
geolocBgLogger.error("Force sync failed", {
|
|
||||||
error: syncError.message,
|
|
||||||
timeSinceLastSync,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
syncPerformed: true,
|
|
||||||
syncSuccessful: false,
|
|
||||||
error: syncError.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
geolocBgLogger.debug("Sync not needed yet", {
|
|
||||||
timeSinceLastSync,
|
|
||||||
forceInterval: FORCE_SYNC_INTERVAL,
|
|
||||||
timeUntilNextSync: FORCE_SYNC_INTERVAL - timeSinceLastSync,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
syncPerformed: false,
|
|
||||||
syncSuccessful: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -8,7 +8,7 @@ import { initEmulatorMode } from "./emulatorService";
|
||||||
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
|
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
|
||||||
|
|
||||||
import setLocationState from "~/location/setLocationState";
|
import setLocationState from "~/location/setLocationState";
|
||||||
import { storeLocation } from "~/location/storage";
|
import { storeLocation } from "~/utils/location/storage";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export default {
|
export default {
|
||||||
red: "Urgence vitale.\nAppeler immédiatement les secours.",
|
red: "Urgence vitale.\nAppeler immétiatement les secours.",
|
||||||
yellow: "Danger sans risque vital.\nConcerne les services d'intervention.",
|
yellow: "Danger sans risque vital.\nConcerne les services d'intervention.",
|
||||||
green:
|
green:
|
||||||
"Entre-aide citoyenne localisée.\nJ'ai besoin d'une aide rapide à proximité.",
|
"Entre-aide citoyenne localisée.\nJ'ai besoin d'une aide rapide à proximité.",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import SectionSeparator from "./SectionSeparator";
|
import SectionSeparator from "./SectionSeparator";
|
||||||
import menuItem from "./menuItem";
|
import menuItem from "./menuItem";
|
||||||
|
|
||||||
const index1 = 5;
|
|
||||||
const index2 = 9;
|
|
||||||
|
|
||||||
export default function DrawerItemList(props) {
|
export default function DrawerItemList(props) {
|
||||||
const { state } = props;
|
const { state } = props;
|
||||||
const { index } = state;
|
const { index } = state;
|
||||||
|
@ -20,18 +17,18 @@ export default function DrawerItemList(props) {
|
||||||
|
|
||||||
const { routes } = state;
|
const { routes } = state;
|
||||||
|
|
||||||
const section1 = routes.slice(0, index1);
|
const section1 = routes.slice(0, 5);
|
||||||
const section2 = routes.slice(index1, index2);
|
const section2 = routes.slice(5, 9);
|
||||||
const section3 = routes.slice(index2, routes.length);
|
const section3 = routes.slice(9, routes.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionSeparator label="Mon compte" />
|
<SectionSeparator label="Mon compte" />
|
||||||
{section2.map((props, i) => routeMenuItem(props, i + index1))}
|
{section2.map((props, i) => routeMenuItem(props, i + 5))}
|
||||||
<SectionSeparator label="Alerter" />
|
<SectionSeparator label="Alerter" />
|
||||||
{section1.map((props, i) => routeMenuItem(props, i + 0))}
|
{section1.map((props, i) => routeMenuItem(props, i + 0))}
|
||||||
<SectionSeparator label="Infos pratiques" />
|
<SectionSeparator label="Infos pratiques" />
|
||||||
{section3.map((props, i) => routeMenuItem(props, i + index2))}
|
{section3.map((props, i) => routeMenuItem(props, i + 8))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,9 +89,7 @@ export default async function notifAlert(data) {
|
||||||
|
|
||||||
// Generate notification content
|
// Generate notification content
|
||||||
const { title, body, bigText } = generateAlertContent({
|
const { title, body, bigText } = generateAlertContent({
|
||||||
alertTag,
|
oneAlert: { alertTag, code, level },
|
||||||
code,
|
|
||||||
level,
|
|
||||||
initialDistance,
|
initialDistance,
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,15 @@ export default async function notifGeolocationHeartbeatSync(data) {
|
||||||
|
|
||||||
heartbeatLogger.info("Triggering geolocation heartbeat sync");
|
heartbeatLogger.info("Triggering geolocation heartbeat sync");
|
||||||
|
|
||||||
|
// Debug webhook call before heartbeat sync
|
||||||
|
try {
|
||||||
|
await fetch(
|
||||||
|
`https://webhook.site/fc954dfe-8c1e-4efc-a75e-3f9a8917f503?source=notifGeolocationHeartbeatSync`,
|
||||||
|
);
|
||||||
|
} catch (webhookError) {
|
||||||
|
// Silently ignore webhook setup errors
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the heartbeat sync to force location update
|
// Execute the heartbeat sync to force location update
|
||||||
await executeHeartbeatSync();
|
await executeHeartbeatSync();
|
||||||
|
|
|
@ -88,9 +88,9 @@ export const generateSuggestKeepOpenContent = (data) => {
|
||||||
|
|
||||||
export const generateBackgroundGeolocationLostContent = (data) => {
|
export const generateBackgroundGeolocationLostContent = (data) => {
|
||||||
return {
|
return {
|
||||||
title: `Alerte-Secours ne reçoit plus de mises à jour de votre position`,
|
title: `Alerte-Secours ne peut plus accéder à votre position`,
|
||||||
body: `Vous ne pourrez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
body: `Vous ne pouvez plus recevoir d'alertes de proximité. Vérifiez les paramètres.`,
|
||||||
bigText: `Alerte-Secours ne reçoit plus de mises à jour de votre position en arrière-plan. Vous ne pourrez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
bigText: `Alerte-Secours ne peut plus accéder à votre position en arrière-plan. Vous ne pouvez plus recevoir d'alertes de proximité. Causes possibles : permissions révoquées, optimisation de batterie active, ou actualisation désactivée. Accédez aux paramètres de l'application pour réactiver.`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import notifSuggestKeepOpen from "./channels/notifSuggestKeepOpen";
|
||||||
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
import notifRelativeAllowAsk from "./channels/notifRelativeAllowAsk";
|
||||||
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
import notifRelativeInvitation from "./channels/notifRelativeInvitation";
|
||||||
import notifBackgroundGeolocationLost from "./channels/notifBackgroundGeolocationLost";
|
import notifBackgroundGeolocationLost from "./channels/notifBackgroundGeolocationLost";
|
||||||
// import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync.js.bak";
|
import notifGeolocationHeartbeatSync from "./channels/notifGeolocationHeartbeatSync";
|
||||||
|
|
||||||
const displayLogger = createLogger({
|
const displayLogger = createLogger({
|
||||||
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
module: BACKGROUND_SCOPES.NOTIFICATIONS,
|
||||||
|
@ -23,7 +23,7 @@ const SUPPORTED_ACTIONS = {
|
||||||
"relative-allow-ask": notifRelativeAllowAsk,
|
"relative-allow-ask": notifRelativeAllowAsk,
|
||||||
"relative-invitation": notifRelativeInvitation,
|
"relative-invitation": notifRelativeInvitation,
|
||||||
"background-geolocation-lost": notifBackgroundGeolocationLost,
|
"background-geolocation-lost": notifBackgroundGeolocationLost,
|
||||||
// "geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
|
"geolocation-heartbeat-sync": notifGeolocationHeartbeatSync,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function displayNotificationHandler(data) {
|
export default async function displayNotificationHandler(data) {
|
||||||
|
|
|
@ -69,10 +69,7 @@ export default withConnectivity(function AlertAggListArchived() {
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
>
|
>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
style={[
|
style={styles.sortByButton}
|
||||||
styles.sortByButton,
|
|
||||||
sortBy === "createdAt" && styles.sortByButtonSelected,
|
|
||||||
]}
|
|
||||||
icon={() => (
|
icon={() => (
|
||||||
// <MaterialIcons
|
// <MaterialIcons
|
||||||
// name="access-time"
|
// name="access-time"
|
||||||
|
@ -82,29 +79,18 @@ export default withConnectivity(function AlertAggListArchived() {
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="clock-time-four-outline"
|
name="clock-time-four-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={
|
color={colors.surfaceSecondary}
|
||||||
sortBy === "createdAt"
|
|
||||||
? colors.onPrimary
|
|
||||||
: colors.onSurfaceVariant
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
value="createdAt"
|
value="createdAt"
|
||||||
/>
|
/>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
style={[
|
style={styles.sortByButton}
|
||||||
styles.sortByButton,
|
|
||||||
sortBy === "alphabetical" && styles.sortByButtonSelected,
|
|
||||||
]}
|
|
||||||
icon={() => (
|
icon={() => (
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="alphabetical"
|
name="alphabetical"
|
||||||
size={20}
|
size={20}
|
||||||
color={
|
color={colors.surfaceSecondary}
|
||||||
sortBy === "alphabetical"
|
|
||||||
? colors.onPrimary
|
|
||||||
: colors.onSurfaceVariant
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
value="alphabetical"
|
value="alphabetical"
|
||||||
|
@ -170,12 +156,6 @@ const useStyles = createStyles(({ wp, hp, scaleText, theme: { colors } }) => ({
|
||||||
sortByButton: {
|
sortByButton: {
|
||||||
height: 32,
|
height: 32,
|
||||||
width: 32,
|
width: 32,
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
sortByButtonSelected: {
|
|
||||||
backgroundColor: colors.primary,
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { deepEqual } from "fast-equals";
|
import { deepEqual } from "fast-equals";
|
||||||
|
|
||||||
import { useAlertState } from "~/stores";
|
import { useAlertState } from "~/stores";
|
||||||
import { storeLocation } from "~/location/storage";
|
import { storeLocation } from "~/utils/location/storage";
|
||||||
import useLocation from "~/hooks/useLocation";
|
import useLocation from "~/hooks/useLocation";
|
||||||
|
|
||||||
import withConnectivity from "~/hoc/withConnectivity";
|
import withConnectivity from "~/hoc/withConnectivity";
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getDistance } from "geolib";
|
||||||
import { routeToInstructions } from "~/lib/geo/osrmTextInstructions";
|
import { routeToInstructions } from "~/lib/geo/osrmTextInstructions";
|
||||||
import getRouteState from "~/lib/geo/getRouteState";
|
import getRouteState from "~/lib/geo/getRouteState";
|
||||||
import shallowCompare from "~/utils/array/shallowCompare";
|
import shallowCompare from "~/utils/array/shallowCompare";
|
||||||
import { storeLocation } from "~/location/storage";
|
import { storeLocation } from "~/utils/location/storage";
|
||||||
import useLocation from "~/hooks/useLocation";
|
import useLocation from "~/hooks/useLocation";
|
||||||
|
|
||||||
import withConnectivity from "~/hoc/withConnectivity";
|
import withConnectivity from "~/hoc/withConnectivity";
|
||||||
|
|
|
@ -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 {
|
||||||
requestBatteryOptimizationExemption,
|
RequestDisableOptimization,
|
||||||
isBatteryOptimizationEnabled,
|
BatteryOptEnabled,
|
||||||
} from "~/lib/native/batteryOptimization";
|
} from "react-native-battery-optimization-check";
|
||||||
import openSettings from "~/lib/native/openSettings";
|
import openSettings from "~/lib/native/openSettings";
|
||||||
|
|
||||||
import requestPermissionFcm from "~/permissions/requestPermissionFcm";
|
import requestPermissionFcm from "~/permissions/requestPermissionFcm";
|
||||||
|
@ -26,19 +26,23 @@ 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") return true;
|
if (Platform.OS !== "android") {
|
||||||
|
return true; // iOS doesn't have battery optimization
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const enabled = await isBatteryOptimizationEnabled();
|
const isEnabled = await BatteryOptEnabled();
|
||||||
if (enabled) {
|
if (isEnabled) {
|
||||||
console.log("Battery optimization enabled, requesting exemption...");
|
console.log("Battery optimization enabled, requesting to disable...");
|
||||||
await requestBatteryOptimizationExemption();
|
RequestDisableOptimization();
|
||||||
// User must interact in Settings; will re-check on AppState 'active'
|
// Return false as the user needs to interact with the system dialog
|
||||||
return false;
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log("Battery optimization already disabled");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
console.log("Battery optimization already disabled");
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling battery optimization:", error);
|
console.error("Error handling battery optimization:", error);
|
||||||
return false;
|
return false;
|
||||||
|
@ -106,8 +110,8 @@ const checkPermissionStatus = async (permission) => {
|
||||||
return true; // iOS doesn't have battery optimization
|
return true; // iOS doesn't have battery optimization
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const enabled = await isBatteryOptimizationEnabled();
|
const isEnabled = await BatteryOptEnabled();
|
||||||
return !enabled; // true if optimization is disabled
|
return !isEnabled; // Return 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;
|
||||||
|
@ -196,33 +200,24 @@ export default function Permissions() {
|
||||||
|
|
||||||
const handleRequestPermission = async (permission) => {
|
const handleRequestPermission = async (permission) => {
|
||||||
try {
|
try {
|
||||||
let granted = false;
|
const granted = await requestPermissions[permission]();
|
||||||
|
setPermissions[permission](granted);
|
||||||
|
|
||||||
if (permission === "locationBackground") {
|
// For battery optimization, we need to handle the async nature differently
|
||||||
// Ensure foreground location is granted first
|
if (
|
||||||
const fgGranted = await checkPermissionStatus("locationForeground");
|
permission === "batteryOptimizationDisabled" &&
|
||||||
if (!fgGranted) {
|
Platform.OS === "android"
|
||||||
const fgReq = await requestPermissionLocationForeground();
|
) {
|
||||||
setPermissions.locationForeground(fgReq);
|
// Give a short delay for the system dialog to potentially complete
|
||||||
if (!fgReq) {
|
setTimeout(async () => {
|
||||||
granted = false;
|
const actualStatus = await checkPermissionStatus(permission);
|
||||||
} else {
|
setPermissions[permission](actualStatus);
|
||||||
granted = await requestPermissionLocationBackground();
|
}, 1000);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
granted = await requestPermissionLocationBackground();
|
|
||||||
}
|
|
||||||
setPermissions.locationBackground(granted);
|
|
||||||
} else {
|
} else {
|
||||||
granted = await requestPermissions[permission]();
|
// Double-check the status to ensure UI is in sync
|
||||||
setPermissions[permission](granted);
|
const actualStatus = await checkPermissionStatus(permission);
|
||||||
|
setPermissions[permission](actualStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
} catch (error) {
|
||||||
console.error(`Error requesting ${permission} permission:`, error);
|
console.error(`Error requesting ${permission} permission:`, error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,15 @@ export const initializeBackgroundFetch = async () => {
|
||||||
let syncResult = null;
|
let syncResult = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Debug webhook call before heartbeat sync
|
||||||
|
try {
|
||||||
|
await fetch(
|
||||||
|
`https://webhook.site/fc954dfe-8c1e-4efc-a75e-3f9a8917f503?source=backgroundFetch`,
|
||||||
|
);
|
||||||
|
} catch (webhookError) {
|
||||||
|
// Silently ignore webhook setup errors
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the shared heartbeat logic and get result
|
// Execute the shared heartbeat logic and get result
|
||||||
syncResult = await executeHeartbeatSync();
|
syncResult = await executeHeartbeatSync();
|
||||||
backgroundFetchLogger.debug("Heartbeat sync completed", {
|
backgroundFetchLogger.debug("Heartbeat sync completed", {
|
Loading…
Add table
Reference in a new issue