Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 55 additions & 56 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
import { StatusBar } from 'expo-status-bar';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Alert,
AppState,
AppStateStatus,
InteractionManager,
LogBox,
Text,
View,
Alert,
AppState,
AppStateStatus,
InteractionManager,
LogBox,
Text,
View,
} from 'react-native';

import { Asset } from 'expo-asset';
Expand All @@ -26,25 +26,25 @@
import { AuthProvider, useAdaptiveTheme, useReviewMetrics } from './src/hooks';
import AppNavigator from './src/navigation/AppNavigator';
import {
getCacheStatus,
getRevalidatingCacheKeys,
subscribeToCacheStatus
getCacheStatus,
getRevalidatingCacheKeys,
subscribeToCacheStatus

Check failure on line 31 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Module '"./src/services/api"' has no exported member 'subscribeToCacheStatus'. Did you mean to use 'import subscribeToCacheStatus from "./src/services/api"' instead?
} from './src/services/api';
import { warmCriticalCaches } from './src/services/cacheWarming';
import { crashReportingService } from './src/services/crashReporting';
import { featureCapabilities } from './src/services/featureCapabilities';
import {
CRITICAL_FONTS,
fontService,
SECONDARY_FONTS,
CRITICAL_FONTS,
fontService,
SECONDARY_FONTS,
} from './src/services/fontService';
import { inAppReviewService } from './src/services/inAppReview';
import { mobileAuthService } from './src/services/mobileAuth';
import {
registerForPushNotifications, // Added missing native push helpers
registerTokenWithBackend,
removeNotificationListener,
setupForegroundBadgeSync,
registerForPushNotifications, // Added missing native push helpers
registerTokenWithBackend,
removeNotificationListener,
setupForegroundBadgeSync,
} from './src/services/pushNotifications';
import { searchIndexService } from './src/services/searchIndex';
import { checkSessionValidity, initializeSecureStorage } from './src/services/secureStorage';
Expand All @@ -54,11 +54,11 @@
import { waitForHydration } from './src/store/createStore';
import { useDegradationStore } from './src/store/degradationStore';
import {
consumeHydrationResetToast,
subscribeToHydrationResetToast,
consumeHydrationResetToast,
subscribeToHydrationResetToast,
} from './src/store/persistence';
import { handleCacheVersionUpdate } from './src/utils/cacheVersioning';
import { requireEnvVariables } from './src/utils/env';

Check failure on line 61 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find module './src/utils/env' or its corresponding type declarations.
import { appLogger } from './src/utils/logger';

// Keep the splash screen visible while we fetch resources
Expand Down Expand Up @@ -172,6 +172,7 @@
useReviewMetrics();

const appStateRef = useRef<AppStateStatus>(AppState.currentState);
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const [appIsReady, setAppIsReady] = React.useState(false);
const [showPreferencesResetToast, setShowPreferencesResetToast] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
Expand Down Expand Up @@ -214,7 +215,7 @@
try {
await Promise.all([
fontService.loadFonts(allFonts),
Asset.loadAsync(CRITICAL_ASSETS),

Check failure on line 218 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Argument of type 'readonly [any, any, any]' is not assignable to parameter of type 'string | number | string[] | number[]'.
]);
} catch (e: any) {
crashReportingService.reportError(e, 'font-loading-error');
Expand Down Expand Up @@ -258,29 +259,7 @@
}
}, []);

useEffect(() => {
if (!appIsReady) return;

// #848: track previous state locally so this listener detects foreground
// transitions on its own, rather than depending on another effect to keep
// a shared ref current. The subscription is removed on cleanup below.
let previousState = AppState.currentState;
const appStateSubscription = AppState.addEventListener('change', nextAppState => {
const wasInBackground = previousState.match(/inactive|background/);
const isForegrounded = nextAppState === 'active';
if (wasInBackground && isForegrounded) {
void checkForOtaUpdate();
}
previousState = nextAppState;
});

// Check once on first foreground after app ready
void checkForOtaUpdate();

return () => {
appStateSubscription.remove();
};
}, [appIsReady, checkForOtaUpdate]);

const handleOtaUpdate = useCallback(async () => {
try {
Expand Down Expand Up @@ -348,8 +327,8 @@
Object.entries(capabilities).forEach(([feature, info]) => {
if (feature !== 'checkedAt' && 'status' in info) {
// #807: isFeatureType narrows string key to FeatureType
if ((Object.values(FeatureType) as string[]).includes(feature)) {

Check failure on line 330 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find name 'FeatureType'.
degradationStore.setFeatureStatus(feature as FeatureType, info.status);

Check failure on line 331 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find name 'FeatureType'.
}
}
});
Expand Down Expand Up @@ -394,8 +373,8 @@
Object.entries(capabilities).forEach(([feature, info]) => {
if (feature !== 'checkedAt' && 'status' in info) {
// #807: isFeatureType narrows string key to FeatureType
if ((Object.values(FeatureType) as string[]).includes(feature)) {

Check failure on line 376 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find name 'FeatureType'.
degradationStore.setFeatureStatus(feature as FeatureType, info.status);

Check failure on line 377 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find name 'FeatureType'.
}
}
});
Expand Down Expand Up @@ -457,7 +436,7 @@
// Issue #820: read store directly rather than closed-over component state.
const store = useNotificationStore.getState();
store.addNotification({
id: notification.request.identifier,

Check failure on line 439 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Object literal may only specify known properties, and 'id' does not exist in type 'Omit<StoredNotification, "id" | "read" | "receivedAt">'.
type: (notification.request.content.data?.type as any) ?? 'general',
title: notification.request.content.title ?? '',
body: notification.request.content.body ?? '',
Expand Down Expand Up @@ -504,6 +483,10 @@
}, []);

useEffect(() => {
// This effect consolidates all AppState 'change' listeners to prevent race
// conditions and duplicate calls when the app is rapidly backgrounded and
// foregrounded. A 200ms debounce is used to handle these events.

const checkSessionOnForeground = async () => {
// Don't read the store before it has rehydrated — destructured actions
// would be undefined and calling them would throw / silently no-op.
Expand Down Expand Up @@ -555,29 +538,45 @@
await useDeviceStore.getState().runDeviceCompromisedCheck();
};

// Wait for the persisted store to rehydrate before the first session check
// so we never read store actions before they exist.
void waitForHydration(useAppStore).then(() => {
void checkSessionOnForeground();
});
checkCompromisedOnForeground();
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}

const appStateSubscription = AppState.addEventListener('change', nextAppState => {
const wasInBackground = appStateRef.current.match(/inactive|background/);
const isForegrounded = nextAppState === 'active';
debounceTimerRef.current = setTimeout(() => {
const wasInBackground = appStateRef.current.match(/inactive|background/);
const isForegrounded = nextAppState === 'active';

if (wasInBackground && isForegrounded) {
if (wasInBackground && isForegrounded) {
// All foreground actions are consolidated here
void checkForOtaUpdate();
void checkSessionOnForeground();
void checkCompromisedOnForeground();
}

appStateRef.current = nextAppState;
}, 200);
};

// Initial checks on app ready. The AppState listener will handle subsequent
// foregrounding events.
if (appIsReady) {
void checkForOtaUpdate();
void waitForHydration(useAppStore).then(() => {
void checkSessionOnForeground();
void checkCompromisedOnForeground();
}
});
checkCompromisedOnForeground();
}

appStateRef.current = nextAppState;
});
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);

return () => {
appStateSubscription.remove();
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, []);
}, [appIsReady, checkForOtaUpdate]);

if (!appIsReady) {
return null;
Expand All @@ -588,9 +587,9 @@
<AuthProvider>
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} />
<CacheRevalidationBanner />
<ScreenErrorBoundary screenName="AppNavigator">

Check failure on line 590 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find name 'ScreenErrorBoundary'.
<AppNavigator />
</ScreenErrorBoundary>

Check failure on line 592 in App.tsx

View workflow job for this annotation

GitHub Actions / Syntax & Type Check

Cannot find name 'ScreenErrorBoundary'.
<NotificationPermissionExplanationSheet />
{showPreferencesResetToast ? <PreferencesResetToast /> : null}
<UpdatePromptModal
Expand Down
14 changes: 11 additions & 3 deletions src/services/mobileAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,17 @@ class MobileAuthService {
throw new Error('No refresh token available. Please log in again.');
}

const { data } = await apiClient.post<AuthResult>(ENDPOINTS.REFRESH, {
refreshToken,
});
const { data } = await apiClient.post<AuthResult>(
ENDPOINTS.REFRESH,
{},
{
headers: {
// The refresh token is sent in the Authorization header for security,
// preventing it from being logged in server-side request bodies.
Authorization: `Bearer ${refreshToken}`,
},
}
);

await this._persistSession(data, false);
return data;
Expand Down
Loading
Loading