fix: staging persistence + disconnect and geo auth reload loop

This commit is contained in:
devthejo 2025-05-02 08:59:17 +02:00
parent 91c374756b
commit 40b27bff29
6 changed files with 133 additions and 16 deletions

View file

@ -13,6 +13,9 @@ import { getDeviceUuid } from "./deviceUuid";
export async function registerUser() {
const { data } = await network.apolloClient.mutate({
mutation: REGISTER_USER_MUTATION,
context: {
skipAuth: true, // Skip adding Authorization header
},
});
const authToken = data.addOneAuthInitToken.authTokenJwt;
return { authToken };
@ -27,6 +30,9 @@ export async function loginUserToken({ authToken }) {
phoneModel: Device.modelName,
deviceUuid,
},
context: {
skipAuth: true, // Skip adding Authorization header
},
});
const userToken = data.doAuthLoginToken.userBearerJwt;
return { userToken };

View file

@ -1,4 +1,8 @@
import { Platform } from "react-native";
import { secureStore } from "~/lib/secureStore";
// Key for storing staging setting in secureStore
const STAGING_SETTING_KEY = "env.isStaging";
// Logging configuration
const LOG_SCOPES = process.env.APP_LOG_SCOPES;
@ -85,16 +89,50 @@ const stagingMap = {
IS_STAGING: true,
};
export const setStaging = (enabled) => {
export const setStaging = async (enabled) => {
for (const key of Object.keys(env)) {
if (stagingMap[key] !== undefined) {
env[key] = enabled ? stagingMap[key] : envMap[key];
}
}
// Persist the staging setting
await secureStore.setItemAsync(STAGING_SETTING_KEY, String(enabled));
};
// Initialize with default values
const env = { ...envMap };
// Load the staging setting from secureStore
export const initializeEnv = async () => {
try {
const storedStaging = await secureStore.getItemAsync(STAGING_SETTING_KEY);
if (storedStaging !== null) {
const isStaging = storedStaging === "true";
if (isStaging) {
// Apply staging settings without persisting again
for (const key of Object.keys(env)) {
if (stagingMap[key] !== undefined) {
env[key] = stagingMap[key];
}
}
}
}
} catch (error) {
console.error("Failed to load staging setting from secureStore:", error);
}
};
// Initialize environment settings
// We use an IIFE to handle the async initialization
(async () => {
try {
await initializeEnv();
} catch (error) {
console.error("Failed to initialize environment settings:", error);
}
})();
export default env;
// +1

View file

@ -64,7 +64,31 @@ export default async function trackLocation() {
feature: "tracking",
});
// Track the last time we handled auth changes to prevent rapid successive calls
let lastAuthHandleTime = 0;
const AUTH_HANDLE_COOLDOWN = 3000; // 3 seconds cooldown
// Track the last time we triggered an auth reload to prevent rapid successive calls
let lastAuthReloadTime = 0;
const AUTH_RELOAD_COOLDOWN = 5000; // 5 seconds cooldown
async function handleAuth(userToken) {
// Implement debouncing for auth state changes
const now = Date.now();
const timeSinceLastHandle = now - lastAuthHandleTime;
if (timeSinceLastHandle < AUTH_HANDLE_COOLDOWN) {
locationLogger.info(
"Auth state change handled too recently, debouncing",
{
timeSinceLastHandle,
cooldown: AUTH_HANDLE_COOLDOWN,
},
);
return;
}
lastAuthHandleTime = now;
locationLogger.info("Handling auth token update", {
hasToken: !!userToken,
});
@ -144,13 +168,29 @@ export default async function trackLocation() {
method: response?.method,
isSync: response?.isSync,
});
const statusCode = response?.status;
const now = Date.now();
const timeSinceLastReload = now - lastAuthReloadTime;
switch (statusCode) {
case 410:
// Token expired, logout
locationLogger.info("Auth token expired (410), logging out");
authActions.logout();
break;
case 401:
// Unauthorized, check cooldown before triggering reload
if (timeSinceLastReload < AUTH_RELOAD_COOLDOWN) {
locationLogger.info("Auth reload requested too soon, skipping", {
timeSinceLastReload,
cooldown: AUTH_RELOAD_COOLDOWN,
});
return;
}
locationLogger.info("Refreshing authentication token");
lastAuthReloadTime = now;
authActions.reload(); // should retriger sync in handleAuth via subscribeAuthState when done
break;
}

View file

@ -13,15 +13,17 @@ export default function createAuthLink({ store }) {
const { getAuthState } = store;
const authLink = new ApolloLink((operation, forward) => {
const { userToken } = getAuthState();
const headers = operation.getContext().hasOwnProperty("headers")
? operation.getContext().headers
: {};
if (userToken && headers["X-Hasura-Role"] !== "anonymous") {
setBearerHeader(headers, userToken);
} else {
headers["X-Hasura-Role"] = "anonymous";
const context = operation.getContext();
const headers = context.hasOwnProperty("headers") ? context.headers : {};
// Skip adding auth header if skipAuth flag is set
if (!context.skipAuth) {
const { userToken } = getAuthState();
if (userToken) {
setBearerHeader(headers, userToken);
}
}
// authLinkLogger.debug("Request headers", { headers });
operation.setContext({ headers });
return forward(operation);

View file

@ -95,9 +95,9 @@ export default function Developer() {
<Switch
value={isStaging}
onValueChange={async (value) => {
setStaging(value);
setIsStaging(value);
await reset();
setIsStaging(value); // Update UI immediately
await setStaging(value); // Persist the change
await reset(); // Reset auth state
}}
/>
</View>

View file

@ -132,14 +132,43 @@ export default createAtom(({ get, merge, getActions }) => {
const reload = async () => {
authLogger.info("Reloading auth state");
// Check if we're already reloading or in a loading state
const { isReloading, lastReloadTime } = get();
const now = Date.now();
const timeSinceLastReload = now - lastReloadTime;
const RELOAD_COOLDOWN = 2000; // 2 seconds cooldown
if (isReloading) {
authLogger.info("Auth reload already in progress, skipping");
return true;
}
if (timeSinceLastReload < RELOAD_COOLDOWN) {
authLogger.info("Auth reload requested too soon, skipping", {
timeSinceLastReload,
cooldown: RELOAD_COOLDOWN,
});
return true;
}
if (isLoading()) {
await loadingPromise;
return true;
}
startLoading();
await secureStore.deleteItemAsync("userToken");
await init();
return true;
// Set reloading state
merge({ isReloading: true, lastReloadTime: now });
try {
startLoading();
await secureStore.deleteItemAsync("userToken");
await init();
return true;
} finally {
// Clear reloading state even if there was an error
merge({ isReloading: false });
}
};
const onReload = async () => {
@ -247,6 +276,8 @@ export default createAtom(({ get, merge, getActions }) => {
onReload: false,
onReloadAuthToken: null,
userOffMode: false,
isReloading: false,
lastReloadTime: 0,
},
actions: {
init,