Compare commits

...

20 commits

Author SHA1 Message Date
6378758f9b chore: clean nav logs 2025-07-02 13:10:31 +02:00
47f11d1b88 fix: undefined error 2025-07-02 12:18:34 +02:00
d4de0b4541 fix: undefined error 2025-07-02 12:14:02 +02:00
b5ae235ba4 fix: reduce tracesSampleRate 2025-07-02 01:00:01 +02:00
5bf3f9b6f9 chore: clean sentry in headless 2025-07-02 00:59:06 +02:00
27ead01714 refactor: storage 2025-07-02 00:54:30 +02:00
a83d423c77 chore: clean 2025-07-02 00:23:17 +02:00
9b272bea61 chore: wip 2025-07-01 22:39:45 +02:00
be0cd62cb9 fix: known keys 2025-07-01 22:12:25 +02:00
f39875b810 chore: clean 2025-07-01 18:00:07 +02:00
6e290bdb69 fix: memoryAsyncStorage 2025-07-01 13:40:29 +02:00
0001a50a5f Revert "chore: try to use sync to refresh"
This reverts commit e47a33bcd8.
2025-07-01 13:40:01 +02:00
e47a33bcd8 chore: try to use sync to refresh 2025-07-01 13:39:38 +02:00
6af58755c1 fix: back to stateless refresh (sync endpoint) 2025-06-30 12:36:50 +02:00
b10ff5a6e7 fix: don't handle refresh in headless mode anymore 2025-06-29 23:09:11 +02:00
cf61de639c Revert "fix(headless): use axios instead of apollo for auth"
This reverts commit 644480182d.
2025-06-29 23:04:18 +02:00
7ab708a536 Revert "fix(headless): use fetch instead of axios for auth"
This reverts commit 09ea8cd563.
2025-06-29 21:30:56 +02:00
0ac28515df fix(android): foreground service 2025-06-29 18:58:40 +02:00
09ea8cd563 fix(headless): use fetch instead of axios for auth 2025-06-29 18:49:11 +02:00
644480182d fix(headless): use axios instead of apollo for auth 2025-06-29 18:42:58 +02:00
24 changed files with 425 additions and 463 deletions

View file

@ -3,6 +3,9 @@
<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.CALL_PHONE"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<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"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
@ -44,6 +47,8 @@
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ERROR_RECOVERY_ONLY"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&amp;channel=release"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://expo-updates.alertesecours.fr/api/manifest?project=alerte-secours&amp;channel=release"/>
<service android:name="com.transistorsoft.locationmanager.service.LocationRequestService" android:foregroundServiceType="location|dataSync" android:enabled="true" android:exported="false" tools:replace="android:foregroundServiceType"/>
<service android:name="com.transistorsoft.backgroundfetch.BackgroundFetchService" android:foregroundServiceType="dataSync" android:enabled="true" android:exported="false"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>

187
index.js
View file

@ -20,6 +20,7 @@ import onMessageReceived from "~/notifications/onMessageReceived";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import * as Sentry from "@sentry/react-native"; import * as Sentry from "@sentry/react-native";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
// setup notification, this have to stay in index.js // setup notification, this have to stay in index.js
notifee.onBackgroundEvent(notificationBackgroundEvent); notifee.onBackgroundEvent(notificationBackgroundEvent);
@ -31,14 +32,16 @@ messaging().setBackgroundMessageHandler(onMessageReceived);
registerRootComponent(App); registerRootComponent(App);
// Constants for persistence // Constants for persistence
const LAST_SYNC_TIME_KEY = "@geolocation_last_sync_time";
// const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000; // const FORCE_SYNC_INTERVAL = 24 * 60 * 60 * 1000;
const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING // const FORCE_SYNC_INTERVAL = 60 * 60 * 1000; // DEBUGGING
const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING
// Helper functions for persisting sync time // Helper functions for persisting sync time
const getLastSyncTime = async () => { const getLastSyncTime = async () => {
try { try {
const value = await AsyncStorage.getItem(LAST_SYNC_TIME_KEY); const value = await AsyncStorage.getItem(
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
);
return value ? parseInt(value, 10) : Date.now(); return value ? parseInt(value, 10) : Date.now();
} catch (error) { } catch (error) {
Sentry.captureException(error, { Sentry.captureException(error, {
@ -50,7 +53,10 @@ const getLastSyncTime = async () => {
const setLastSyncTime = async (time) => { const setLastSyncTime = async (time) => {
try { try {
await AsyncStorage.setItem(LAST_SYNC_TIME_KEY, time.toString()); await AsyncStorage.setItem(
STORAGE_KEYS.GEOLOCATION_LAST_SYNC_TIME,
time.toString(),
);
} catch (error) { } catch (error) {
Sentry.captureException(error, { Sentry.captureException(error, {
tags: { module: "headless-task", operation: "set-last-sync-time" }, tags: { module: "headless-task", operation: "set-last-sync-time" },
@ -119,111 +125,24 @@ const HeadlessTask = async (event) => {
throw new Error("Invalid event name received"); throw new Error("Invalid event name received");
} }
// Add initial breadcrumb
Sentry.addBreadcrumb({
message: "HeadlessTask started",
category: "headless-task",
level: "info",
data: {
eventName: name,
params: params ? JSON.stringify(params) : null,
timestamp: Date.now(),
},
});
geolocBgLogger.info("HeadlessTask event received", { name, params }); geolocBgLogger.info("HeadlessTask event received", { name, params });
switch (name) { switch (name) {
case "heartbeat": case "heartbeat":
// Add breadcrumb for heartbeat event
Sentry.addBreadcrumb({
message: "Heartbeat event received",
category: "headless-task",
level: "info",
timestamp: Date.now() / 1000,
});
// Get persisted last sync time // Get persisted last sync time
const lastSyncTime = await getLastSyncTime(); const lastSyncTime = await getLastSyncTime();
const now = Date.now(); const now = Date.now();
const timeSinceLastSync = now - lastSyncTime; const timeSinceLastSync = now - lastSyncTime;
// Add context about sync timing
Sentry.setContext("sync-timing", {
lastSyncTime: new Date(lastSyncTime).toISOString(),
currentTime: new Date(now).toISOString(),
timeSinceLastSync: timeSinceLastSync,
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
});
Sentry.addBreadcrumb({
message: "Sync timing calculated",
category: "headless-task",
level: "info",
data: {
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
needsForceSync: timeSinceLastSync >= FORCE_SYNC_INTERVAL,
},
});
// Get current position with performance tracking // Get current position with performance tracking
const locationStartTime = Date.now();
const location = await getCurrentPosition(); const location = await getCurrentPosition();
const locationDuration = Date.now() - locationStartTime;
const isLocationError = location && location.code !== undefined;
Sentry.addBreadcrumb({
message: "getCurrentPosition completed",
category: "headless-task",
level: isLocationError ? "warning" : "info",
data: {
success: !isLocationError,
error: isLocationError ? location : undefined,
coords: !isLocationError ? location?.coords : undefined,
},
});
geolocBgLogger.debug("getCurrentPosition result", { location }); geolocBgLogger.debug("getCurrentPosition result", { location });
if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) { if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) {
geolocBgLogger.info("Forcing location sync after 24h"); geolocBgLogger.info("Forcing location sync after 24h");
Sentry.addBreadcrumb({
message: "Force sync triggered",
category: "headless-task",
level: "info",
data: {
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
},
});
try { try {
// Get pending records count before sync with timeout
const pendingCount = await Promise.race([
BackgroundGeolocation.getCount(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("getCount timeout")), 10000),
),
]);
Sentry.addBreadcrumb({
message: "Pending records count",
category: "headless-task",
level: "info",
data: { pendingCount },
});
// Change pace to ensure location updates with timeout // Change pace to ensure location updates with timeout
await Promise.race([ await Promise.race([
BackgroundGeolocation.changePace(true), BackgroundGeolocation.changePace(true),
@ -235,12 +154,6 @@ const HeadlessTask = async (event) => {
), ),
]); ]);
Sentry.addBreadcrumb({
message: "changePace completed",
category: "headless-task",
level: "info",
});
// Perform sync with timeout // Perform sync with timeout
const syncResult = await Promise.race([ const syncResult = await Promise.race([
BackgroundGeolocation.sync(), BackgroundGeolocation.sync(),
@ -249,26 +162,8 @@ const HeadlessTask = async (event) => {
), ),
]); ]);
Sentry.addBreadcrumb({
message: "Sync completed successfully",
category: "headless-task",
level: "info",
data: {
syncResult: Array.isArray(syncResult)
? `${syncResult.length} records`
: "completed",
},
});
// Update last sync time after successful sync // Update last sync time after successful sync
await setLastSyncTime(now); await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "Last sync time updated",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncError) { } catch (syncError) {
Sentry.captureException(syncError, { Sentry.captureException(syncError, {
tags: { tags: {
@ -286,22 +181,6 @@ const HeadlessTask = async (event) => {
geolocBgLogger.error("Force sync failed", { error: syncError }); geolocBgLogger.error("Force sync failed", { error: syncError });
} }
} else {
Sentry.addBreadcrumb({
message: "Force sync not needed",
category: "headless-task",
level: "info",
data: {
timeSinceLastSyncHours: (
timeSinceLastSync /
(1000 * 60 * 60)
).toFixed(2),
nextSyncInHours: (
(FORCE_SYNC_INTERVAL - timeSinceLastSync) /
(1000 * 60 * 60)
).toFixed(2),
},
});
} }
break; break;
@ -312,17 +191,6 @@ const HeadlessTask = async (event) => {
break; break;
} }
Sentry.addBreadcrumb({
message: "Location update received",
category: "headless-task",
level: "info",
data: {
coords: params.location?.coords,
activity: params.location?.activity,
hasLocation: !!params.location,
},
});
geolocBgLogger.debug("Location update received", { geolocBgLogger.debug("Location update received", {
location: params.location, location: params.location,
}); });
@ -338,17 +206,6 @@ const HeadlessTask = async (event) => {
const httpStatus = params.response?.status; const httpStatus = params.response?.status;
const isHttpSuccess = httpStatus === 200; const isHttpSuccess = httpStatus === 200;
Sentry.addBreadcrumb({
message: "HTTP response received",
category: "headless-task",
level: isHttpSuccess ? "info" : "warning",
data: {
status: httpStatus,
success: params.response?.success,
hasResponse: !!params.response,
},
});
geolocBgLogger.debug("HTTP response received", { geolocBgLogger.debug("HTTP response received", {
response: params.response, response: params.response,
}); });
@ -358,13 +215,6 @@ const HeadlessTask = async (event) => {
try { try {
const now = Date.now(); const now = Date.now();
await setLastSyncTime(now); await setLastSyncTime(now);
Sentry.addBreadcrumb({
message: "Last sync time updated (HTTP success)",
category: "headless-task",
level: "info",
data: { newSyncTime: new Date(now).toISOString() },
});
} catch (syncTimeError) { } catch (syncTimeError) {
geolocBgLogger.error("Failed to update sync time", { geolocBgLogger.error("Failed to update sync time", {
error: syncTimeError, error: syncTimeError,
@ -381,26 +231,11 @@ const HeadlessTask = async (event) => {
break; break;
default: default:
Sentry.addBreadcrumb({ break;
message: "Unknown event type",
category: "headless-task",
level: "warning",
data: { eventName: name },
});
} }
// Task completed successfully // Task completed successfully
const taskDuration = Date.now() - taskStartTime; const taskDuration = Date.now() - taskStartTime;
Sentry.addBreadcrumb({
message: "HeadlessTask completed successfully",
category: "headless-task",
level: "info",
data: {
eventName: name,
duration: taskDuration,
},
});
} catch (error) { } catch (error) {
const taskDuration = Date.now() - taskStartTime; const taskDuration = Date.now() - taskStartTime;

View file

@ -186,7 +186,11 @@
F7ADCC68A8E44BA69FCA849E /* Fix Xcode 15 Bug */, F7ADCC68A8E44BA69FCA849E /* Fix Xcode 15 Bug */,
B1AB92A327A24FB294681EDD /* Fix Xcode 15 Bug */, B1AB92A327A24FB294681EDD /* Fix Xcode 15 Bug */,
0E26E4D25E2E49C3AB2723FA /* Fix Xcode 15 Bug */, 0E26E4D25E2E49C3AB2723FA /* Fix Xcode 15 Bug */,
8589214E888941E1817F4C9F /* Remove signature files (Xcode workaround) */, 5D0A324371BA4A5385A92DF5 /* Fix Xcode 15 Bug */,
40472AFA41A8495E9D557630 /* Fix Xcode 15 Bug */,
771057F6078145908B36B18B /* Fix Xcode 15 Bug */,
7C1CC306C4DF48D4B5E1BDFB /* Fix Xcode 15 Bug */,
2401E852B4D64D59BD803280 /* Remove signature files (Xcode workaround) */,
); );
buildRules = ( buildRules = (
); );
@ -1099,6 +1103,142 @@ 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\";
";
};
5D0A324371BA4A5385A92DF5 /* 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";
};
C637A42109E14A1AA86AF639 /* 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\";
";
};
40472AFA41A8495E9D557630 /* 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";
};
B79BB2C3F48A4CC4B6830286 /* 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\";
";
};
771057F6078145908B36B18B /* 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";
};
83ACE65C55FE44EC820FD39A /* 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\";
";
};
7C1CC306C4DF48D4B5E1BDFB /* 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";
};
2401E852B4D64D59BD803280 /* 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 */

View file

@ -7,8 +7,8 @@ import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
import { authActions, permissionWizardActions } from "~/stores"; import { authActions, permissionWizardActions } from "~/stores";
import { secureStore } from "~/lib/memorySecureStore"; import { secureStore } from "~/storage/memorySecureStore";
import memoryAsyncStorage from "~/lib/memoryAsyncStorage"; import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
import "~/lib/mapbox"; import "~/lib/mapbox";
import "~/i18n"; import "~/i18n";

View file

@ -1,4 +1,5 @@
import { secureStore } from "~/lib/memorySecureStore"; import { secureStore } from "~/storage/memorySecureStore";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import uuidGenerator from "react-native-uuid"; import uuidGenerator from "react-native-uuid";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { FEATURE_SCOPES } from "~/lib/logger/scopes"; import { FEATURE_SCOPES } from "~/lib/logger/scopes";
@ -21,12 +22,12 @@ async function getDeviceUuid() {
// Create a new promise for this generation attempt // Create a new promise for this generation attempt
uuidGenerationPromise = (async () => { uuidGenerationPromise = (async () => {
try { try {
let deviceUuid = await secureStore.getItemAsync("deviceUuid"); let deviceUuid = await secureStore.getItemAsync(STORAGE_KEYS.DEVICE_UUID);
if (!deviceUuid) { if (!deviceUuid) {
deviceLogger.info("No device UUID found, generating new one"); deviceLogger.info("No device UUID found, generating new one");
deviceUuid = uuidGenerator.v4(); deviceUuid = uuidGenerator.v4();
await secureStore.setItemAsync("deviceUuid", deviceUuid); await secureStore.setItemAsync(STORAGE_KEYS.DEVICE_UUID, deviceUuid);
deviceLogger.info("New device UUID generated and stored", { deviceLogger.info("New device UUID generated and stored", {
uuid: deviceUuid.substring(0, 8) + "...", uuid: deviceUuid.substring(0, 8) + "...",
}); });

View file

@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { View, ScrollView, StyleSheet, Platform } from "react-native"; import { View, ScrollView, StyleSheet, Platform } from "react-native";
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import Text from "../Text"; import Text from "../Text";
@ -64,14 +65,12 @@ Ce Contrat constitue l'intégralité de l'accord entre vous et nous concernant l
Si vous avez des questions concernant ce Contrat, veuillez nous contacter à : Si vous avez des questions concernant ce Contrat, veuillez nous contacter à :
Email : contact@alertesecours.fr`; Email : contact@alertesecours.fr`;
const EULA_STORAGE_KEY = "@eula_accepted";
const EULA = ({ onAccept, visible = true }) => { const EULA = ({ onAccept, visible = true }) => {
if (!visible || Platform.OS !== "ios") return null; if (!visible || Platform.OS !== "ios") return null;
const handleAccept = async () => { const handleAccept = async () => {
try { try {
await AsyncStorage.setItem(EULA_STORAGE_KEY, "true"); await AsyncStorage.setItem(STORAGE_KEYS.EULA_ACCEPTED, "true");
onAccept(); onAccept();
} catch (error) { } catch (error) {
console.error("Error saving EULA acceptance:", error); console.error("Error saving EULA acceptance:", error);

View file

@ -11,8 +11,8 @@ import {
usePermissionWizardState, usePermissionWizardState,
useNetworkState, useNetworkState,
} from "~/stores"; } from "~/stores";
import { secureStore } from "~/lib/memorySecureStore"; import { secureStore } from "~/storage/memorySecureStore";
import memoryAsyncStorage from "~/lib/memoryAsyncStorage"; import memoryAsyncStorage from "~/storage/memoryAsyncStorage";
import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground"; import requestPermissionLocationBackground from "~/permissions/requestPermissionLocationBackground";
import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground"; import requestPermissionLocationForeground from "~/permissions/requestPermissionLocationForeground";

View file

@ -1,8 +1,6 @@
import { Platform } from "react-native"; import { Platform } from "react-native";
import { secureStore } from "~/lib/secureStore"; import { secureStore } from "~/storage/memorySecureStore";
import { STORAGE_KEYS } from "~/storage/storageKeys";
// Key for storing staging setting in secureStore
const STAGING_SETTING_KEY = "env.isStaging";
// Logging configuration // Logging configuration
const LOG_SCOPES = process.env.APP_LOG_SCOPES; const LOG_SCOPES = process.env.APP_LOG_SCOPES;
@ -97,7 +95,7 @@ export const setStaging = async (enabled) => {
} }
// Persist the staging setting // Persist the staging setting
await secureStore.setItemAsync(STAGING_SETTING_KEY, String(enabled)); await secureStore.setItemAsync(STORAGE_KEYS.ENV_IS_STAGING, String(enabled));
}; };
// Initialize with default values // Initialize with default values
@ -106,7 +104,9 @@ const env = { ...envMap };
// Load the staging setting from secureStore // Load the staging setting from secureStore
export const initializeEnv = async () => { export const initializeEnv = async () => {
try { try {
const storedStaging = await secureStore.getItemAsync(STAGING_SETTING_KEY); const storedStaging = await secureStore.getItemAsync(
STORAGE_KEYS.ENV_IS_STAGING,
);
if (storedStaging !== null) { if (storedStaging !== null) {
const isStaging = storedStaging === "true"; const isStaging = storedStaging === "true";
if (isStaging) { if (isStaging) {

View file

@ -1,9 +1,8 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import { Platform } from "react-native"; import { Platform } from "react-native";
const EULA_STORAGE_KEY = "@eula_accepted";
export const useEULA = () => { export const useEULA = () => {
const [eulaAccepted, setEulaAccepted] = useState(true); const [eulaAccepted, setEulaAccepted] = useState(true);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -16,7 +15,7 @@ export const useEULA = () => {
const checkEULA = async () => { const checkEULA = async () => {
try { try {
const accepted = await AsyncStorage.getItem(EULA_STORAGE_KEY); const accepted = await AsyncStorage.getItem(STORAGE_KEYS.EULA_ACCEPTED);
setEulaAccepted(!!accepted); setEulaAccepted(!!accepted);
} catch (error) { } catch (error) {
console.error("Error checking EULA status:", error); console.error("Error checking EULA status:", error);

View file

@ -1,10 +1,9 @@
import BackgroundGeolocation from "react-native-background-geolocation"; import BackgroundGeolocation from "react-native-background-geolocation";
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes"; import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
const EMULATOR_MODE_KEY = "emulator_mode_enabled";
// Global variables // Global variables
let emulatorIntervalId = null; let emulatorIntervalId = null;
let isEmulatorModeEnabled = false; let isEmulatorModeEnabled = false;
@ -18,7 +17,9 @@ const emulatorLogger = createLogger({
// Initialize emulator mode based on stored preference // Initialize emulator mode based on stored preference
export const initEmulatorMode = async () => { export const initEmulatorMode = async () => {
try { try {
const storedValue = await AsyncStorage.getItem(EMULATOR_MODE_KEY); const storedValue = await AsyncStorage.getItem(
STORAGE_KEYS.EMULATOR_MODE_ENABLED,
);
emulatorLogger.debug("Initializing emulator mode", { storedValue }); emulatorLogger.debug("Initializing emulator mode", { storedValue });
if (storedValue === "true") { if (storedValue === "true") {
@ -58,7 +59,7 @@ export const enableEmulatorMode = async () => {
isEmulatorModeEnabled = true; isEmulatorModeEnabled = true;
// Persist the setting // Persist the setting
await AsyncStorage.setItem(EMULATOR_MODE_KEY, "true"); await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "true");
emulatorLogger.debug("Emulator mode setting saved"); emulatorLogger.debug("Emulator mode setting saved");
} catch (error) { } catch (error) {
emulatorLogger.error("Failed to enable emulator mode", { emulatorLogger.error("Failed to enable emulator mode", {
@ -81,7 +82,7 @@ export const disableEmulatorMode = async () => {
// Persist the setting // Persist the setting
try { try {
await AsyncStorage.setItem(EMULATOR_MODE_KEY, "false"); await AsyncStorage.setItem(STORAGE_KEYS.EMULATOR_MODE_ENABLED, "false");
emulatorLogger.debug("Emulator mode setting saved"); emulatorLogger.debug("Emulator mode setting saved");
} catch (error) { } catch (error) {
emulatorLogger.error("Failed to save emulator mode setting", { emulatorLogger.error("Failed to save emulator mode setting", {

View file

@ -4,17 +4,8 @@ import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes"; import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode";
import { initEmulatorMode } from "./emulatorService"; import { initEmulatorMode } from "./emulatorService";
import * as Sentry from "@sentry/react-native";
import { SPAN_STATUS_OK, SPAN_STATUS_ERROR } from "@sentry/react-native";
import throttle from "lodash.throttle"; import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
import {
getAuthState,
subscribeAuthState,
authActions,
permissionsActions,
} from "~/stores";
import setLocationState from "~/location/setLocationState"; import setLocationState from "~/location/setLocationState";
import { storeLocation } from "~/utils/location/storage"; import { storeLocation } from "~/utils/location/storage";
@ -76,9 +67,6 @@ export default async function trackLocation() {
isStaging: env.IS_STAGING, isStaging: env.IS_STAGING,
}); });
// Throttling configuration for auth reload only
const AUTH_RELOAD_THROTTLE = 5000; // 5 seconds throttle
// Handle auth function - no throttling or cooldown // Handle auth function - no throttling or cooldown
async function handleAuth(userToken) { async function handleAuth(userToken) {
locationLogger.info("Handling auth token update", { locationLogger.info("Handling auth token update", {
@ -108,25 +96,6 @@ export default async function trackLocation() {
}, },
); );
// Verify the current configuration
try {
const currentConfig = await BackgroundGeolocation.getConfig();
locationLogger.debug("Current background geolocation config", {
hasHeaders: !!currentConfig.headers,
headerKeys: currentConfig.headers
? Object.keys(currentConfig.headers)
: [],
authHeader: currentConfig.headers?.Authorization
? currentConfig.headers.Authorization.substring(0, 15) + "..."
: "Not set",
url: currentConfig.url,
});
} catch (error) {
locationLogger.error("Failed to get background geolocation config", {
error: error.message,
});
}
const state = await BackgroundGeolocation.getState(); const state = await BackgroundGeolocation.getState();
try { try {
const decodedToken = jwtDecode(userToken); const decodedToken = jwtDecode(userToken);
@ -171,19 +140,6 @@ export default async function trackLocation() {
battery: location.battery, battery: location.battery,
}); });
// Add Sentry breadcrumb for location updates
Sentry.addBreadcrumb({
message: "Location update in trackLocation",
category: "geolocation",
level: "info",
data: {
coords: location.coords,
activity: location.activity?.type,
battery: location.battery?.level,
isMoving: location.isMoving,
},
});
if ( if (
location.coords && location.coords &&
location.coords.latitude && location.coords.latitude &&
@ -195,100 +151,12 @@ export default async function trackLocation() {
} }
}); });
// The core auth reload function that will be throttled
function _reloadAuth() {
locationLogger.info("Refreshing authentication token");
authActions.reload(); // should retriger sync in handleAuth via subscribeAuthState when done
}
// Create throttled version of auth reload with lodash
const reloadAuth = throttle(_reloadAuth, AUTH_RELOAD_THROTTLE, {
leading: true,
trailing: false, // Prevent trailing calls to avoid duplicate refreshes
});
BackgroundGeolocation.onHttp(async (response) => { BackgroundGeolocation.onHttp(async (response) => {
// Log the full response including headers if available // log status code and response
locationLogger.debug("HTTP response received", { locationLogger.debug("HTTP response received", {
status: response?.status, status: response?.status,
success: response?.success,
responseText: response?.responseText,
url: response?.url,
method: response?.method,
isSync: response?.isSync,
requestHeaders:
response?.request?.headers || "Headers not available in response",
});
// Add Sentry breadcrumb for HTTP responses
Sentry.addBreadcrumb({
message: "Background geolocation HTTP response",
category: "geolocation-http",
level: response?.status === 200 ? "info" : "warning",
data: {
status: response?.status,
success: response?.success,
url: response?.url,
isSync: response?.isSync,
recordCount: response?.count,
},
});
// Log the current auth token for comparison
const { userToken } = getAuthState();
locationLogger.debug("Current auth state token", {
tokenAvailable: !!userToken,
tokenPrefix: userToken ? userToken.substring(0, 10) + "..." : null,
});
const statusCode = response?.status;
switch (statusCode) {
case 410:
// Token expired, logout
locationLogger.info("Auth token expired (410), logging out");
Sentry.addBreadcrumb({
message: "Auth token expired - logging out",
category: "geolocation-auth",
level: "warning",
});
authActions.logout();
break;
case 401:
// Unauthorized, use throttled reload
locationLogger.info("Unauthorized (401), attempting to refresh token");
// Add more detailed logging of the error response
try {
const errorBody = response?.responseText
? JSON.parse(response.responseText)
: null;
locationLogger.debug("Unauthorized error details", {
errorBody,
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
errorPath: errorBody?.error?.errors?.[0]?.path,
});
Sentry.addBreadcrumb({
message: "Unauthorized - refreshing token",
category: "geolocation-auth",
level: "warning",
data: {
errorType: errorBody?.error?.type,
errorMessage: errorBody?.error?.message,
},
});
} catch (e) {
locationLogger.debug("Failed to parse error response", {
error: e.message,
responseText: response?.responseText, responseText: response?.responseText,
}); });
}
reloadAuth();
break;
}
}); });
try { try {

View file

@ -9,15 +9,15 @@ export default function getStatusCode({ networkError, graphQLErrors }) {
if (graphQLErrors) { if (graphQLErrors) {
let code; let code;
for (const err of graphQLErrors) { for (const err of graphQLErrors) {
if (err.extensions.http) { if (err.extensions?.http) {
code = err.extensions.http; code = err.extensions.http;
break; break;
} }
if (err.extensions.statusCode) { if (err.extensions?.statusCode) {
code = err.extensions.statusCode; code = err.extensions.statusCode;
break; break;
} }
if (err.extensions.code) { if (err.extensions?.code) {
code = err.extensions.code; code = err.extensions.code;
break; break;
} }

View file

@ -25,7 +25,7 @@ const getReleaseVersion = () => {
Sentry.init({ Sentry.init({
dsn: env.SENTRY_DSN, dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0, tracesSampleRate: 0.1,
debug: __DEV__, debug: __DEV__,
// Configure release to match ios-archive.sh format // Configure release to match ios-archive.sh format
release: getReleaseVersion(), release: getReleaseVersion(),

View file

@ -1,6 +1,7 @@
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
import { getAsyncStorageKeys } from "./storageKeys";
const storageLogger = createLogger({ const storageLogger = createLogger({
module: SYSTEM_SCOPES.STORAGE, module: SYSTEM_SCOPES.STORAGE,
@ -29,15 +30,8 @@ export const memoryAsyncStorage = {
storageLogger.info("Initializing memory async storage"); storageLogger.info("Initializing memory async storage");
// List of known keys that need to be cached // Get all registered AsyncStorage keys from the registry
const knownKeys = [ const knownKeys = getAsyncStorageKeys();
"permission_wizard_completed",
"override_messages",
"last_known_location",
"eula_accepted",
"last_update_check",
"emulator_mode_enabled",
];
// Load all known keys into memory // Load all known keys into memory
for (const key of knownKeys) { for (const key of knownKeys) {
@ -152,6 +146,7 @@ export const memoryAsyncStorage = {
storageLogger.debug("Set in memory cache", { key }); storageLogger.debug("Set in memory cache", { key });
// Try to persist to AsyncStorage // Try to persist to AsyncStorage
(async () => {
try { try {
await AsyncStorage.setItem(key, value); await AsyncStorage.setItem(key, value);
storageLogger.debug("Persisted to AsyncStorage", { key }); storageLogger.debug("Persisted to AsyncStorage", { key });
@ -163,8 +158,8 @@ export const memoryAsyncStorage = {
error: error.message, error: error.message,
}, },
); );
// Continue - value is at least in memory
} }
})();
}, },
/** /**
@ -178,6 +173,7 @@ export const memoryAsyncStorage = {
storageLogger.debug("Deleted from memory cache", { key }); storageLogger.debug("Deleted from memory cache", { key });
// Try to delete from AsyncStorage // Try to delete from AsyncStorage
(async () => {
try { try {
await AsyncStorage.removeItem(key); await AsyncStorage.removeItem(key);
storageLogger.debug("Deleted from AsyncStorage", { key }); storageLogger.debug("Deleted from AsyncStorage", { key });
@ -188,6 +184,7 @@ export const memoryAsyncStorage = {
}); });
// Continue - at least removed from memory // Continue - at least removed from memory
} }
})();
}, },
/** /**
@ -242,6 +239,7 @@ export const memoryAsyncStorage = {
storageLogger.info("Cleared memory cache"); storageLogger.info("Cleared memory cache");
// Try to clear AsyncStorage // Try to clear AsyncStorage
(async () => {
try { try {
await AsyncStorage.clear(); await AsyncStorage.clear();
storageLogger.info("Cleared AsyncStorage"); storageLogger.info("Cleared AsyncStorage");
@ -250,6 +248,7 @@ export const memoryAsyncStorage = {
error: error.message, error: error.message,
}); });
} }
})();
}, },
/** /**

View file

@ -1,6 +1,7 @@
import { secureStore as originalSecureStore } from "./secureStore"; import { secureStore as originalSecureStore } from "./secureStore";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
import { getSecureStoreKeys } from "./storageKeys";
const storageLogger = createLogger({ const storageLogger = createLogger({
module: SYSTEM_SCOPES.STORAGE, module: SYSTEM_SCOPES.STORAGE,
@ -29,16 +30,8 @@ export const memorySecureStore = {
storageLogger.info("Initializing memory secure store"); storageLogger.info("Initializing memory secure store");
// List of known keys that need to be cached // Get all registered secure store keys from the registry
const knownKeys = [ const knownKeys = getSecureStoreKeys();
"deviceUuid",
"authToken",
"userToken",
"dev.authToken",
"dev.userToken",
"anon.authToken",
"anon.userToken",
];
// Load all known keys into memory // Load all known keys into memory
for (const key of knownKeys) { for (const key of knownKeys) {

View file

@ -0,0 +1,83 @@
/**
* Storage Keys Registry
*
* This file maintains a registry of all storage keys used throughout the application.
* By defining keys as constants here, they are automatically included in memory storage
* initialization, eliminating the need for manual maintenance of key lists.
*/
const secureStoreKeys = new Set();
const asyncStorageKeys = new Set();
/**
* Register a secure store key and return it as a constant
* @param {string} key - The storage key to register for secure store
* @returns {string} The same key, now registered for secure store
*/
export const registerSecureStoreKey = (key) => {
secureStoreKeys.add(key);
return key;
};
/**
* Register an AsyncStorage key and return it as a constant
* @param {string} key - The storage key to register for AsyncStorage
* @returns {string} The same key, now registered for AsyncStorage
*/
export const registerAsyncStorageKey = (key) => {
asyncStorageKeys.add(key);
return key;
};
/**
* Get all secure store keys
* @returns {string[]} Array of secure store keys
*/
export const getSecureStoreKeys = () => Array.from(secureStoreKeys);
/**
* Get all AsyncStorage keys
* @returns {string[]} Array of AsyncStorage keys
*/
export const getAsyncStorageKeys = () => Array.from(asyncStorageKeys);
/**
* Get all registered storage keys (both types)
* @returns {string[]} Array of all registered keys
*/
export const getAllRegisteredKeys = () => [
...Array.from(secureStoreKeys),
...Array.from(asyncStorageKeys),
];
/**
* Storage key constants
* All storage keys used throughout the application should be defined here.
*/
export const STORAGE_KEYS = {
// Secure Store Keys - Authentication & Security
DEVICE_UUID: registerSecureStoreKey("deviceUuid"),
AUTH_TOKEN: registerSecureStoreKey("authToken"),
USER_TOKEN: registerSecureStoreKey("userToken"),
DEV_AUTH_TOKEN: registerSecureStoreKey("dev.authToken"),
DEV_USER_TOKEN: registerSecureStoreKey("dev.userToken"),
ANON_AUTH_TOKEN: registerSecureStoreKey("anon.authToken"),
ANON_USER_TOKEN: registerSecureStoreKey("anon.userToken"),
FCM_TOKEN_STORED: registerSecureStoreKey("fcmTokenStored"),
FCM_TOKEN_STORED_DEVICE_ID: registerSecureStoreKey("fcmTokenStoredDeviceId"),
ENV_IS_STAGING: registerSecureStoreKey("env.isStaging"),
// AsyncStorage Keys - App State & Preferences
GEOLOCATION_LAST_SYNC_TIME: registerAsyncStorageKey(
"@geolocation_last_sync_time",
),
EULA_ACCEPTED: registerAsyncStorageKey("@eula_accepted"),
OVERRIDE_MESSAGES: registerAsyncStorageKey("@override_messages"),
PERMISSION_WIZARD_COMPLETED: registerAsyncStorageKey(
"@permission_wizard_completed",
),
LAST_UPDATE_CHECK_TIME: registerAsyncStorageKey("lastUpdateCheckTime"),
LAST_KNOWN_LOCATION: registerAsyncStorageKey("@last_known_location"),
EULA_ACCEPTED_SIMPLE: registerAsyncStorageKey("eula_accepted"),
EMULATOR_MODE_ENABLED: registerAsyncStorageKey("emulator_mode_enabled"),
};

View file

@ -1,8 +1,7 @@
import { createAtom } from "~/lib/atomic-zustand"; import { createAtom } from "~/lib/atomic-zustand";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
const OVERRIDE_MESSAGES_STORAGE_KEY = "@override_messages";
export default createAtom(({ merge, set, get, reset }) => { export default createAtom(({ merge, set, get, reset }) => {
const overrideMessagesCache = {}; const overrideMessagesCache = {};
@ -10,7 +9,7 @@ export default createAtom(({ merge, set, get, reset }) => {
const initCache = async () => { const initCache = async () => {
try { try {
const storedData = await AsyncStorage.getItem( const storedData = await AsyncStorage.getItem(
OVERRIDE_MESSAGES_STORAGE_KEY, STORAGE_KEYS.OVERRIDE_MESSAGES,
); );
const storedMessages = storedData ? JSON.parse(storedData) : {}; const storedMessages = storedData ? JSON.parse(storedData) : {};
Object.entries(storedMessages).forEach(([messageId, data]) => { Object.entries(storedMessages).forEach(([messageId, data]) => {
@ -24,7 +23,7 @@ export default createAtom(({ merge, set, get, reset }) => {
const saveOverrideMessagesToStorage = async () => { const saveOverrideMessagesToStorage = async () => {
try { try {
await AsyncStorage.setItem( await AsyncStorage.setItem(
OVERRIDE_MESSAGES_STORAGE_KEY, STORAGE_KEYS.OVERRIDE_MESSAGES,
JSON.stringify(overrideMessagesCache), JSON.stringify(overrideMessagesCache),
); );
} catch (error) { } catch (error) {

View file

@ -1,4 +1,5 @@
import { secureStore } from "~/lib/memorySecureStore"; import { secureStore } from "~/storage/memorySecureStore";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import jwtDecode from "jwt-decode"; import jwtDecode from "jwt-decode";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { FEATURE_SCOPES } from "~/lib/logger/scopes"; import { FEATURE_SCOPES } from "~/lib/logger/scopes";
@ -10,13 +11,13 @@ import isExpired from "~/lib/time/isExpired";
import { registerUser, loginUserToken } from "~/auth/actions"; import { registerUser, loginUserToken } from "~/auth/actions";
// DEV // DEV
// SecureStore.deleteItemAsync("userToken"); // SecureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN);
// SecureStore.deleteItemAsync("authToken"); // SecureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN);
// SecureStore.deleteItemAsync("dev.userToken"); // SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN);
// SecureStore.deleteItemAsync("dev.authToken"); // SecureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN);
// SecureStore.deleteItemAsync("anon.userToken"); // SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN);
// SecureStore.deleteItemAsync("anon.authToken"); // SecureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN);
// SecureStore.getItemAsync("userToken").then((t) => authLogger.debug("User token", { token: t })); // SecureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN).then((t) => authLogger.debug("User token", { token: t }));
const authLogger = createLogger({ const authLogger = createLogger({
module: FEATURE_SCOPES.AUTH, module: FEATURE_SCOPES.AUTH,
@ -68,7 +69,7 @@ export default createAtom(({ get, merge, getActions }) => {
authLogger.info("Attempting to login with auth token"); authLogger.info("Attempting to login with auth token");
const { userToken } = await loginUserToken({ authToken }); const { userToken } = await loginUserToken({ authToken });
authLogger.info("Successfully obtained user token"); authLogger.info("Successfully obtained user token");
await secureStore.setItemAsync("userToken", userToken); await secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, userToken);
endLoading({ endLoading({
userToken, userToken,
}); });
@ -81,8 +82,8 @@ export default createAtom(({ get, merge, getActions }) => {
"Auth token expired, clearing tokens and reinitializing", "Auth token expired, clearing tokens and reinitializing",
); );
await Promise.all([ await Promise.all([
secureStore.deleteItemAsync("authToken"), secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN),
secureStore.deleteItemAsync("userToken"), secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN),
]); ]);
return init(); return init();
} }
@ -93,8 +94,8 @@ export default createAtom(({ get, merge, getActions }) => {
const init = async () => { const init = async () => {
authLogger.debug("Initializing auth state"); authLogger.debug("Initializing auth state");
let { userToken, authToken } = await promiseObject({ let { userToken, authToken } = await promiseObject({
userToken: secureStore.getItemAsync("userToken"), userToken: secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
authToken: secureStore.getItemAsync("authToken"), authToken: secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
}); });
// await delay(5); // await delay(5);
// authLogger.debug("Auth tokens", { userToken, authToken }); // authLogger.debug("Auth tokens", { userToken, authToken });
@ -121,7 +122,7 @@ export default createAtom(({ get, merge, getActions }) => {
const res = await registerUser(); const res = await registerUser();
authLogger.info("Successfully registered new user"); authLogger.info("Successfully registered new user");
authToken = res.authToken; authToken = res.authToken;
await secureStore.setItemAsync("authToken", authToken); await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken);
} }
if (!userToken && authToken) { if (!userToken && authToken) {
@ -165,7 +166,7 @@ export default createAtom(({ get, merge, getActions }) => {
startLoading(); startLoading();
authLogger.debug("Deleting userToken for refresh"); authLogger.debug("Deleting userToken for refresh");
await secureStore.deleteItemAsync("userToken"); await secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN);
await init(); await init();
return true; return true;
@ -183,7 +184,7 @@ export default createAtom(({ get, merge, getActions }) => {
const { onReloadAuthToken: authToken } = get(); const { onReloadAuthToken: authToken } = get();
if (authToken) { if (authToken) {
await secureStore.setItemAsync("authToken", authToken); await secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, authToken);
await loadUserJWT(authToken); await loadUserJWT(authToken);
} else { } else {
await init(); await init();
@ -204,12 +205,12 @@ export default createAtom(({ get, merge, getActions }) => {
if (!isConnected) { if (!isConnected) {
// backup anon tokens // backup anon tokens
const [anonAuthToken, anonUserToken] = await Promise.all([ const [anonAuthToken, anonUserToken] = await Promise.all([
secureStore.getItemAsync("authToken"), secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
secureStore.getItemAsync("userToken"), secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
]); ]);
await Promise.all([ await Promise.all([
secureStore.setItemAsync("anon.authToken", anonAuthToken), secureStore.setItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN, anonAuthToken),
secureStore.setItemAsync("anon.userToken", anonUserToken), secureStore.setItemAsync(STORAGE_KEYS.ANON_USER_TOKEN, anonUserToken),
]); ]);
} }
merge({ onReloadAuthToken: authTokenJwt }); merge({ onReloadAuthToken: authTokenJwt });
@ -219,12 +220,12 @@ export default createAtom(({ get, merge, getActions }) => {
const impersonate = async ({ authTokenJwt }) => { const impersonate = async ({ authTokenJwt }) => {
authLogger.info("Starting impersonation"); authLogger.info("Starting impersonation");
const [anonAuthToken, anonUserToken] = await Promise.all([ const [anonAuthToken, anonUserToken] = await Promise.all([
secureStore.getItemAsync("authToken"), secureStore.getItemAsync(STORAGE_KEYS.AUTH_TOKEN),
secureStore.getItemAsync("userToken"), secureStore.getItemAsync(STORAGE_KEYS.USER_TOKEN),
]); ]);
await Promise.all([ await Promise.all([
secureStore.setItemAsync("dev.authToken", anonAuthToken), secureStore.setItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN, anonAuthToken),
secureStore.setItemAsync("dev.userToken", anonUserToken), secureStore.setItemAsync(STORAGE_KEYS.DEV_USER_TOKEN, anonUserToken),
]); ]);
merge({ onReloadAuthToken: authTokenJwt }); merge({ onReloadAuthToken: authTokenJwt });
triggerReload(); triggerReload();
@ -234,29 +235,29 @@ export default createAtom(({ get, merge, getActions }) => {
authLogger.info("Initiating logout"); authLogger.info("Initiating logout");
const [devAuthToken, devUserToken, anonAuthToken, anonUserToken] = const [devAuthToken, devUserToken, anonAuthToken, anonUserToken] =
await Promise.all([ await Promise.all([
secureStore.getItemAsync("dev.authToken"), secureStore.getItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN),
secureStore.getItemAsync("dev.userToken"), secureStore.getItemAsync(STORAGE_KEYS.DEV_USER_TOKEN),
secureStore.getItemAsync("anon.authToken"), secureStore.getItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN),
secureStore.getItemAsync("anon.userToken"), secureStore.getItemAsync(STORAGE_KEYS.ANON_USER_TOKEN),
]); ]);
if (devAuthToken && devUserToken) { if (devAuthToken && devUserToken) {
await Promise.all([ await Promise.all([
secureStore.setItemAsync("authToken", devAuthToken), secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, devAuthToken),
secureStore.setItemAsync("userToken", devUserToken), secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, devUserToken),
secureStore.deleteItemAsync("dev.authToken"), secureStore.deleteItemAsync(STORAGE_KEYS.DEV_AUTH_TOKEN),
secureStore.deleteItemAsync("dev.userToken"), secureStore.deleteItemAsync(STORAGE_KEYS.DEV_USER_TOKEN),
]); ]);
} else if (anonAuthToken && anonUserToken) { } else if (anonAuthToken && anonUserToken) {
await Promise.all([ await Promise.all([
secureStore.setItemAsync("authToken", anonAuthToken), secureStore.setItemAsync(STORAGE_KEYS.AUTH_TOKEN, anonAuthToken),
secureStore.setItemAsync("userToken", anonUserToken), secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, anonUserToken),
secureStore.deleteItemAsync("anon.authToken"), secureStore.deleteItemAsync(STORAGE_KEYS.ANON_AUTH_TOKEN),
secureStore.deleteItemAsync("anon.userToken"), secureStore.deleteItemAsync(STORAGE_KEYS.ANON_USER_TOKEN),
]); ]);
} else { } else {
await Promise.all([ await Promise.all([
secureStore.deleteItemAsync("authToken"), secureStore.deleteItemAsync(STORAGE_KEYS.AUTH_TOKEN),
secureStore.deleteItemAsync("userToken"), secureStore.deleteItemAsync(STORAGE_KEYS.USER_TOKEN),
]); ]);
merge({ merge({
userOffMode: true, userOffMode: true,
@ -275,6 +276,31 @@ export default createAtom(({ get, merge, getActions }) => {
triggerReload(); triggerReload();
}; };
const setUserToken = async (userToken) => {
authLogger.info("Setting user token", {
hasToken: !!userToken,
});
try {
// Update secure storage
await secureStore.setItemAsync(STORAGE_KEYS.USER_TOKEN, userToken);
// Update in-memory state
merge({ userToken });
// Update session from JWT
if (userToken) {
const jwtData = jwtDecode(userToken);
sessionActions.loadSessionFromJWT(jwtData);
}
authLogger.debug("User token updated successfully");
} catch (error) {
authLogger.error("Failed to set user token", { error: error.message });
throw error;
}
};
return { return {
default: { default: {
userToken: null, userToken: null,
@ -294,6 +320,7 @@ export default createAtom(({ get, merge, getActions }) => {
logout, logout,
onReload, onReload,
userOnMode, userOnMode,
setUserToken,
}, },
}; };
}); });

View file

@ -1,5 +1,6 @@
import { createAtom } from "~/lib/atomic-zustand"; import { createAtom } from "~/lib/atomic-zustand";
import { secureStore } from "~/lib/secureStore"; import { secureStore } from "~/storage/memorySecureStore";
import { STORAGE_KEYS } from "~/storage/storageKeys";
export default createAtom(({ merge, reset }) => { export default createAtom(({ merge, reset }) => {
const setFcmToken = (token) => { const setFcmToken = (token) => {
@ -9,8 +10,11 @@ export default createAtom(({ merge, reset }) => {
}; };
const setFcmTokenStored = ({ fcmToken, deviceId }) => { const setFcmTokenStored = ({ fcmToken, deviceId }) => {
secureStore.setItemAsync("fcmTokenStored", fcmToken); secureStore.setItemAsync(STORAGE_KEYS.FCM_TOKEN_STORED, fcmToken);
secureStore.setItemAsync("fcmTokenStoredDeviceId", deviceId.toString()); secureStore.setItemAsync(
STORAGE_KEYS.FCM_TOKEN_STORED_DEVICE_ID,
deviceId.toString(),
);
merge({ merge({
fcmTokenStored: fcmToken, fcmTokenStored: fcmToken,
deviceId, deviceId,
@ -18,9 +22,11 @@ export default createAtom(({ merge, reset }) => {
}; };
const init = async () => { const init = async () => {
const fcmTokenStored = await secureStore.getItemAsync("fcmTokenStored"); const fcmTokenStored = await secureStore.getItemAsync(
STORAGE_KEYS.FCM_TOKEN_STORED,
);
const fcmTokenStoredDeviceId = await secureStore.getItemAsync( const fcmTokenStoredDeviceId = await secureStore.getItemAsync(
"fcmTokenStoredDeviceId", STORAGE_KEYS.FCM_TOKEN_STORED_DEVICE_ID,
); );
const deviceId = fcmTokenStoredDeviceId const deviceId = fcmTokenStoredDeviceId
? parseInt(fcmTokenStoredDeviceId, 10) ? parseInt(fcmTokenStoredDeviceId, 10)

View file

@ -61,7 +61,7 @@ export default createAtom(({ get, merge, reset }) => {
...m, ...m,
routeName, routeName,
}); });
navLogger.info("Route updated", { routeName }); navLogger.debug("Route updated", { routeName });
}; };
const initialValues = { const initialValues = {
@ -78,11 +78,11 @@ export default createAtom(({ get, merge, reset }) => {
default: initialValues, default: initialValues,
actions: { actions: {
reset: () => { reset: () => {
navLogger.info("Resetting navigation state to initial values"); navLogger.debug("Resetting navigation state to initial values");
reset(); reset();
}, },
updateRouteFromRootStack: (state) => { updateRouteFromRootStack: (state) => {
navLogger.info("Updating route from root stack", { state }); navLogger.debug("Updating route from root stack", { state });
const { index, routeNames } = state; const { index, routeNames } = state;
const rootRouteName = routeNames[index]; const rootRouteName = routeNames[index];
updateRoute({ updateRoute({
@ -90,7 +90,7 @@ export default createAtom(({ get, merge, reset }) => {
}); });
}, },
updateRouteFromDrawer: (state) => { updateRouteFromDrawer: (state) => {
navLogger.info("Updating route from drawer", { state }); navLogger.debug("Updating route from drawer", { state });
const { index, routeNames } = state; const { index, routeNames } = state;
const drawerRouteName = routeNames[index]; const drawerRouteName = routeNames[index];
updateRoute({ updateRoute({
@ -98,7 +98,7 @@ export default createAtom(({ get, merge, reset }) => {
}); });
}, },
updateRouteFromMain: (state) => { updateRouteFromMain: (state) => {
navLogger.info("Updating route from main", { state }); navLogger.debug("Updating route from main", { state });
const { index, routeNames } = state; const { index, routeNames } = state;
const mainRouteName = routeNames[index]; const mainRouteName = routeNames[index];
updateRoute({ updateRoute({
@ -106,13 +106,13 @@ export default createAtom(({ get, merge, reset }) => {
}); });
}, },
setNextNavigation: (nextNavigation) => { setNextNavigation: (nextNavigation) => {
navLogger.info("Setting next navigation", { nextNavigation }); navLogger.debug("Setting next navigation", { nextNavigation });
merge({ merge({
nextNavigation, nextNavigation,
}); });
}, },
setMessageViewFocus: (isFocused, alertId = null) => { setMessageViewFocus: (isFocused, alertId = null) => {
navLogger.info("Setting message view focus", { isFocused, alertId }); navLogger.debug("Setting message view focus", { isFocused, alertId });
merge({ merge({
isOnMessageView: isFocused, isOnMessageView: isFocused,
currentMessageAlertId: isFocused ? alertId : null, currentMessageAlertId: isFocused ? alertId : null,

View file

@ -1,12 +1,13 @@
import { createAtom } from "~/lib/atomic-zustand"; import { createAtom } from "~/lib/atomic-zustand";
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
const WIZARD_COMPLETED_KEY = "@permission_wizard_completed";
export default createAtom(({ set, get }) => { export default createAtom(({ set, get }) => {
const init = async () => { const init = async () => {
try { try {
const wizardCompleted = await AsyncStorage.getItem(WIZARD_COMPLETED_KEY); const wizardCompleted = await AsyncStorage.getItem(
STORAGE_KEYS.PERMISSION_WIZARD_COMPLETED,
);
if (wizardCompleted === "true") { if (wizardCompleted === "true") {
set("completed", true); set("completed", true);
} }
@ -27,7 +28,10 @@ export default createAtom(({ set, get }) => {
setCompleted: (completed) => { setCompleted: (completed) => {
set("completed", completed); set("completed", completed);
if (completed) { if (completed) {
AsyncStorage.setItem(WIZARD_COMPLETED_KEY, "true").catch((error) => { AsyncStorage.setItem(
STORAGE_KEYS.PERMISSION_WIZARD_COMPLETED,
"true",
).catch((error) => {
console.error("Error saving permission wizard status:", error); console.error("Error saving permission wizard status:", error);
}); });
} }

View file

@ -1,14 +1,13 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Alert } from "react-native"; import { Alert } from "react-native";
import * as Updates from "expo-updates"; import * as Updates from "expo-updates";
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import useNow from "~/hooks/useNow"; import useNow from "~/hooks/useNow";
import * as Sentry from "@sentry/react-native"; import * as Sentry from "@sentry/react-native";
import env from "~/env"; import env from "~/env";
import { treeActions } from "~/stores"; import { treeActions } from "~/stores";
const LAST_UPDATE_CHECK_KEY = "lastUpdateCheckTime";
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
const applyUpdate = async () => { const applyUpdate = async () => {
@ -28,12 +27,17 @@ const checkForUpdate = async () => {
return; return;
} }
try { try {
const lastCheckString = await AsyncStorage.getItem(LAST_UPDATE_CHECK_KEY); const lastCheckString = await AsyncStorage.getItem(
STORAGE_KEYS.LAST_UPDATE_CHECK_TIME,
);
const lastCheck = lastCheckString ? new Date(lastCheckString) : null; const lastCheck = lastCheckString ? new Date(lastCheckString) : null;
const nowDate = new Date(); const nowDate = new Date();
if (!lastCheck || nowDate - lastCheck > UPDATE_CHECK_INTERVAL) { if (!lastCheck || nowDate - lastCheck > UPDATE_CHECK_INTERVAL) {
await AsyncStorage.setItem(LAST_UPDATE_CHECK_KEY, nowDate.toISOString()); await AsyncStorage.setItem(
STORAGE_KEYS.LAST_UPDATE_CHECK_TIME,
nowDate.toISOString(),
);
const update = await Updates.checkForUpdateAsync(); const update = await Updates.checkForUpdateAsync();
if (!update.isAvailable) { if (!update.isAvailable) {

View file

@ -1,4 +1,5 @@
import AsyncStorage from "~/lib/memoryAsyncStorage"; import AsyncStorage from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { SYSTEM_SCOPES } from "~/lib/logger/scopes"; import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
@ -7,8 +8,6 @@ const storageLogger = createLogger({
feature: "location-cache", feature: "location-cache",
}); });
export const LOCATION_STORAGE_KEY = "@last_known_location";
/** /**
* Stores location data in AsyncStorage with timestamp * Stores location data in AsyncStorage with timestamp
* @param {Object} coords - Location coordinates object * @param {Object} coords - Location coordinates object
@ -36,7 +35,7 @@ export async function storeLocation(
}); });
await AsyncStorage.setItem( await AsyncStorage.setItem(
LOCATION_STORAGE_KEY, STORAGE_KEYS.LAST_KNOWN_LOCATION,
JSON.stringify({ JSON.stringify({
coords, coords,
timestamp, timestamp,
@ -58,7 +57,7 @@ export async function storeLocation(
export async function getStoredLocation() { export async function getStoredLocation() {
try { try {
storageLogger.debug("Retrieving stored location data"); storageLogger.debug("Retrieving stored location data");
const stored = await AsyncStorage.getItem(LOCATION_STORAGE_KEY); const stored = await AsyncStorage.getItem(STORAGE_KEYS.LAST_KNOWN_LOCATION);
if (!stored) { if (!stored) {
storageLogger.debug("No stored location data found"); storageLogger.debug("No stored location data found");