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
270 changes: 267 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,268 @@
import socketService from './src/services/socket';
import { syncService } from './src/services/syncService';
import { StatusBar } from 'expo-status-bar';
import React, { useEffect, useRef } from 'react';
import { Alert, AppState, AppStateStatus, LogBox } from 'react-native';

import StorybookUI from './.rnstorybook';
import './global.css';

import * as Font from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { ErrorBoundary } from './src/components/common/ErrorBoundary';
import { initializeLogging } from './src/config/logging';
import { AuthProvider, useAdaptiveTheme, useReviewMetrics } from './src/hooks';
import AppNavigator from './src/navigation/AppNavigator';
import { setupNotificationNavigation } from './src/navigation/linking';
import { apiClient } from './src/services/api';
import { crashReportingService } from './src/services/cashReporting';
import { featureCapabilities } from './src/services/featureCapabilities';
import { inAppReviewService } from './src/services/inAppReview';
import { useReviewMetrics } from './src/hooks/useInAppReview';
import { mobileAuthService } from './src/services/mobileAuth';
import {
addNotificationReceivedListener,
getLastNotificationResponse,
removeNotificationListener,
registerForPushNotifications, // Added missing native push helpers
registerTokenWithBackend,
} from './src/services/pushNotifications';
import { requestQueue } from './src/services/requestQueue';
import socketService from './src/services/socket';
import { syncService } from './src/services/syncService'; // Fixed naming convention from the merge conflict
import { useAppStore, useNotificationStore } from './src/store'; // Added missing store imports
import { useDegradationStore } from './src/store/degradationStore';
import { handleCacheVersionUpdate } from './src/utils/cacheVersioning';
import { requireEnvVariables } from './src/utils/env';
import { appLogger } from './src/utils/logger';
import { handleNotificationReceived } from './src/utils/notificationHandlers';
import { initializeSecureStorage } from './src/services/secureStorage'; // Added missing storage helper mock path

// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();

// SHOW_STORYBOOK flag based on environment variable
const SHOW_STORYBOOK = process.env.EXPO_PUBLIC_STORYBOOK === 'true';

// Centralized structured logging initialized on startup
requireEnvVariables();

// Initialize centralized logging on app start
initializeLogging().catch(err => {
console.error('[App] Failed to initialize logging:', err);
});

if (__DEV__) {
appLogger.infoSync('Development mode: centralized logger active');
LogBox.ignoreLogs(['Non-serializable values were found in the navigation state']);
} else {
// Strip all logs except errors in production for performance
console.log = () => {};
console.info = () => {};
console.warn = () => {};
console.debug = () => {};
}

const App = () => {
const theme = useAppStore((state) => state.theme);
useAdaptiveTheme();
// Using imported hook from the merge logic if needed downstream
useReviewMetrics();

const appStateRef = useRef<AppStateStatus>(AppState.currentState);
const [appIsReady, setAppIsReady] = React.useState(false);

useEffect(() => {
async function prepareApp() {
try {
// 1. Load fonts
await Font.loadAsync({
// You can add custom fonts here later if needed
});

// 2. Version-based cache invalidation: clear stale caches on app/data version bump
const appVersion = require('./package.json').version as string;
await handleCacheVersionUpdate(appVersion);

// 3. Initial data fetch (simulate or add real fetch)
await new Promise(resolve => setTimeout(resolve, 500));
} catch (e) {
console.warn('Error during app initialization:', e);
} finally {
setAppIsReady(true);
await SplashScreen.hideAsync();
}
}

prepareApp();
}, []);

const SESSION_REFRESH_WINDOW_MS = 5 * 60 * 1000;

useEffect(() => {
// Initialize crash reporting at app startup
crashReportingService.init();

// Initialize secure storage (Keychain/Keystore) for encrypted token storage
initializeSecureStorage().catch((error) => {
appLogger.errorSync('Failed to initialize secure storage:', error); // Fixed 'logger.error' to 'appLogger.errorSync'
});

// Add global handler for unhandled promise rejections
const unhandledRejectionHandler = (reason: any) => {
const error = reason instanceof Error ? reason : new Error(String(reason));
appLogger.errorSync('Unhandled Promise Rejection', error);
crashReportingService.reportError(error, 'UnhandledPromiseRejection');
};

// Register unhandled rejection listener
if (global.onunhandledrejection === undefined) {
// @ts-ignore - Setting global error handler
global.onunhandledrejection = unhandledRejectionHandler;
}

// Connect to socket when app starts
socketService.connect();

// Initialize feature capability detection (non-blocking)
featureCapabilities.checkAllCapabilities()
.then(capabilities => {
const degradationStore = useDegradationStore.getState();
appLogger.infoSync('[App] Feature capabilities checked', {
camera: capabilities.camera.status,
notifications: capabilities.pushNotifications.status,
location: capabilities.location.status,
});
// Update degradation store with current feature statuses
Object.entries(capabilities).forEach(([feature, info]) => {
if (feature !== 'checkedAt' && 'status' in info) {
degradationStore.setFeatureStatus(feature as any, info.status);
}
});
})
.catch(error => {
appLogger.errorSync('[App] Error checking feature capabilities', error instanceof Error ? error : new Error(String(error)));
});

// Initialize push notifications: request permissions and get device token
registerForPushNotifications().then(async (token) => {
if (token) {
const { setPushToken, setTokenRegistered } = useNotificationStore.getState();
setPushToken(token);
const registered = await registerTokenWithBackend(token);
setTokenRegistered(registered);
}
});

// Start request queue monitoring
requestQueue.startMonitoring(apiClient);

// Initialize and start sync service for background sync
syncService.startAutoSync();

// Initialize In-App Review metrics if applicable
inAppReviewService.init?.();

// Set up notification navigation handler
const notificationCleanup = setupNotificationNavigation();

// Listen for notifications received while app is foregrounded
const subscription = addNotificationReceivedListener(handleNotificationReceived);

// Check if app was launched from a notification
getLastNotificationResponse().then(response => {
if (response) {
appLogger.infoSync('App launched from notification', { response });
}
});

// Cleanup on unmount
return () => {
socketService.disconnect();
syncService.stopAutoSync();
notificationCleanup();
removeNotificationListener(subscription);
// Clean up the unhandled rejection handler
// @ts-ignore
global.onunhandledrejection = undefined;
};
}, []);

useEffect(() => {
const checkSessionOnForeground = async () => {
const {
isAuthenticated,
refreshToken,
sessionExpiresAt,
setUser,
setTokens,
setSessionExpiringSoon,
logout,
} = useAppStore.getState();

if (!isAuthenticated || !refreshToken || !sessionExpiresAt) {
return;
}

const now = Date.now();
const msUntilExpiry = sessionExpiresAt - now;

if (msUntilExpiry <= 0) {
logout();
Alert.alert('Session expired', 'Your session has expired. Please log in again.');
return;
}

if (msUntilExpiry <= SESSION_REFRESH_WINDOW_MS) {
setSessionExpiringSoon(true);
Alert.alert('Session expiring soon', 'Refreshing your session to keep you signed in.');

try {
const refreshedSession = await mobileAuthService.refreshSession();
setUser(refreshedSession.user);
setTokens(
refreshedSession.tokens.accessToken,
refreshedSession.tokens.refreshToken,
refreshedSession.tokens.expiresAt
);
setSessionExpiringSoon(false);
} catch (error) {
appLogger.errorSync('Failed to refresh session on app foreground', error as Error);
logout();
Alert.alert('Session expired', 'We could not refresh your session. Please log in again.');
}
} else {
setSessionExpiringSoon(false);
}
};

checkSessionOnForeground();

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

if (wasInBackground && isForegrounded) {
void checkSessionOnForeground();
}

appStateRef.current = nextAppState;
});

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

if (!appIsReady) {
return null;
}

return (
<ErrorBoundary>
<AuthProvider>
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} />
<AppNavigator />
</AuthProvider>
</ErrorBoundary>
);
};

export default SHOW_STORYBOOK ? StorybookUI : App;
37 changes: 37 additions & 0 deletions docs/GRACEFUL_DEGRADATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Graceful Degradation Strategy

This document outlines TeachLink Mobile's approach to graceful degradation when device features or permissions are unavailable.

## Goals

- Avoid crashes when hardware or permissions are missing.
- Provide clear user feedback and fallback UX.
- Maintain core app functionality with degraded capabilities.

## Features Covered

- Camera (photo capture & gallery)
- Push Notifications
- Location

## Strategy

1. Detect capabilities at startup using `src/services/featureCapabilities.ts`.
2. Persist degradation state in `src/store/degradationStore.ts`.
3. Provide hooks with fallbacks: `src/hooks/useCamera.ts`, `src/hooks/useLocation.ts`.
4. Provide UI components to inform users: `src/components/DegradationBanner.tsx`.
5. Use `locationService` to attempt GPS, then cached, then manual entry.
6. Use in-app notifications when push notifications unavailable.

## Developer Notes

- When adding new features that require hardware or permissions, update `featureCapabilities` and `degradationStore` accordingly.
- Use `degradationStore.addNotification()` to notify users about degraded features.
- Prefer non-blocking initialization; detect capabilities asynchronously.

## Testing

- Test on simulator to verify push notification degradation behavior.
- Deny permissions to test camera fallback to library and manual location entry.
- Test devices without GPS to ensure manual flow works.

Loading
Loading