fix(mobile): auth-gated hydrates and telemetry flush on background

- Run notes/workspace/inbox hydrate, broadcast/survey polling, and offline
  queue flush timers only when hasBootstrapped && isAuthenticated
- Split effects: kill switch, bootstrap, initPlatform, global AppState flush
- Avoid pre-auth 401s from parallel API hydrates

Made-with: Cursor
This commit is contained in:
Saravana Achu Mac 2026-03-31 01:51:05 -07:00
parent e920d724e4
commit 6620e5dabb

View File

@ -3,10 +3,10 @@ import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { AppState, Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { useAuthStore, type AuthState } from '../store/auth-store';
import { useInboxStore, type InboxState } from '../store/inbox-store';
import { useNotesStore, type NotesState } from '../store/notes-store';
import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store';
import { checkKillSwitch, initPlatform } from '../lib/platform';
import { useInboxStore } from '../store/inbox-store';
import { useNotesStore } from '../store/notes-store';
import { useWorkspaceStore } from '../store/workspace-store';
import { checkKillSwitch, flushTelemetry, initPlatform } from '../lib/platform';
import { getBroadcastClient } from '../lib/broadcast-client';
import { getSurveyClient } from '../lib/survey-client';
import { flushNoteQueue, getNoteQueueSize } from '../lib/offline-queue';
@ -37,9 +37,8 @@ export default function RootLayout() {
});
const bootstrapAuth = useAuthStore((state: AuthState) => state.bootstrap);
const hydrateInbox = useInboxStore((state: InboxState) => state.hydrate);
const hydrateNotes = useNotesStore((state: NotesState) => state.hydrate);
const hydrateWorkspaces = useWorkspaceStore((state: WorkspaceState) => state.hydrate);
const hasBootstrapped = useAuthStore((state: AuthState) => state.hasBootstrapped);
const isAuthenticated = useAuthStore((state: AuthState) => state.isAuthenticated);
function toSurveyAnswer(question: Question, value: string): QuestionAnswer {
if (question.type === 'single_choice' || question.type === 'dropdown') {
@ -99,15 +98,45 @@ export default function RootLayout() {
.catch(() => {
setKillSwitchState({ checked: true, disabled: false, message: null });
});
}, []);
useEffect(() => {
void bootstrapAuth();
}, [bootstrapAuth]);
useEffect(() => {
void initPlatform();
void hydrateNotes();
void hydrateWorkspaces();
void hydrateInbox();
void flushQueuedNoteMutations();
void loadBroadcasts();
void loadSurvey();
}, []);
useEffect(() => {
const sub = AppState.addEventListener('change', (nextState) => {
if (nextState === 'background' || nextState === 'inactive') {
flushTelemetry();
}
});
return () => sub.remove();
}, []);
useEffect(() => {
if (!hasBootstrapped || !isAuthenticated) {
return undefined;
}
let cancelled = false;
void (async () => {
await Promise.all([
useNotesStore.getState().hydrate(),
useWorkspaceStore.getState().hydrate(),
useInboxStore.getState().hydrate(),
]);
if (cancelled) {
return;
}
void flushQueuedNoteMutations();
void loadBroadcasts();
void loadSurvey();
})();
const broadcastTimer = setInterval(() => {
void loadBroadcasts();
@ -124,11 +153,12 @@ export default function RootLayout() {
});
return () => {
cancelled = true;
clearInterval(broadcastTimer);
clearInterval(surveyTimer);
appStateSubscription.remove();
};
}, [bootstrapAuth, hydrateInbox, hydrateNotes, hydrateWorkspaces]);
}, [hasBootstrapped, isAuthenticated]);
if (killSwitchState.checked && killSwitchState.disabled) {
return (