From 40b27bff297cdf70f45c66009275110c91a60269 Mon Sep 17 00:00:00 2001 From: devthejo Date: Fri, 2 May 2025 08:59:17 +0200 Subject: [PATCH] fix: staging persistence + disconnect and geo auth reload loop --- src/auth/actions.js | 6 ++++++ src/env.js | 40 ++++++++++++++++++++++++++++++++++- src/location/trackLocation.js | 40 +++++++++++++++++++++++++++++++++++ src/network/authLink.js | 18 +++++++++------- src/scenes/Developer/index.js | 6 +++--- src/stores/auth.js | 39 ++++++++++++++++++++++++++++++---- 6 files changed, 133 insertions(+), 16 deletions(-) diff --git a/src/auth/actions.js b/src/auth/actions.js index 47b9558..e42f191 100644 --- a/src/auth/actions.js +++ b/src/auth/actions.js @@ -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 }; diff --git a/src/env.js b/src/env.js index 6d020f1..3e3ea2c 100644 --- a/src/env.js +++ b/src/env.js @@ -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 diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js index 6ae4447..314b079 100644 --- a/src/location/trackLocation.js +++ b/src/location/trackLocation.js @@ -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; } diff --git a/src/network/authLink.js b/src/network/authLink.js index 4e0583d..aad66cd 100644 --- a/src/network/authLink.js +++ b/src/network/authLink.js @@ -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); diff --git a/src/scenes/Developer/index.js b/src/scenes/Developer/index.js index 85a720c..8499c95 100644 --- a/src/scenes/Developer/index.js +++ b/src/scenes/Developer/index.js @@ -95,9 +95,9 @@ export default function Developer() { { - setStaging(value); - setIsStaging(value); - await reset(); + setIsStaging(value); // Update UI immediately + await setStaging(value); // Persist the change + await reset(); // Reset auth state }} /> diff --git a/src/stores/auth.js b/src/stores/auth.js index 8c26b20..52d64be 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -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,