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
36 changes: 17 additions & 19 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { StatusBar } from 'expo-status-bar';
import React, { useEffect } from 'react';
import { LogBox } from 'react-native';
import '../assets/global.css';
import { requireEnvVariables } from './src/config/env';
import { ErrorBoundary } from './src/components/common/ErrorBoundary';
import crashReportingService from './src/services/crashReporting';
import socketService from './src/services/socket';
import { useAppStore } from './src/store';
import logger from './src/utils/logger';
import AppNavigator from './src/navigation/AppNavigator';
import { StatusBar } from "expo-status-bar";
import React, { useEffect } from "react";
import { LogBox } from "react-native";
import "./global.css";
import { ErrorBoundary } from "./src/components/common/ErrorBoundary";
import AppNavigator from "./src/navigation/AppNavigator";
import socketService from "./src/services/socket";
import { useAppStore } from "./src/store";

requireEnvVariables();
// Notification imports
import { AuthProvider } from "./src/hooks";
import { setupNotificationNavigation } from "./src/navigation/linking";
import apiClient from "./src/services/api/axios.config";
import requestQueue from "./src/services/api/requestQueue";
import {
addNotificationReceivedListener,
getLastNotificationResponse,
removeNotificationListener,
addNotificationReceivedListener,
getLastNotificationResponse,
removeNotificationListener,
} from "./src/services/pushNotifications";
import { handleNotificationReceived } from "./src/utils/notificationHandlers";

Expand Down Expand Up @@ -75,7 +71,7 @@ export default function App() {
// Check if app was launched from a notification
getLastNotificationResponse().then((response) => {
if (response) {
logger.info("App launched from notification:", response);
console.log("App launched from notification:", response);
}
});

Expand All @@ -92,8 +88,10 @@ export default function App() {

return (
<ErrorBoundary>
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} />
<AppNavigator />
<AuthProvider>
<StatusBar style={theme === "dark" ? "light" : "dark"} />
<AppNavigator />
</AuthProvider>
</ErrorBoundary>
);
}
1 change: 1 addition & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";
6 changes: 4 additions & 2 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './useNotificationPermission';
export * from './useVoiceRecognition';
export { AuthProvider, useAuth } from "./useAuth";
export * from "./useNotificationPermission";
export * from "./useVoiceRecognition";

145 changes: 145 additions & 0 deletions src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import mobileAuth, { AuthUser } from "../services/mobileAuth";

interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
user: AuthUser | null;
}

interface AuthContextType extends AuthState {
login: (credentials: {
email: string;
password: string;
rememberMe?: boolean;
}) => Promise<void>;
loginWithBiometrics: () => Promise<void>;
logout: () => Promise<void>;
restoreSession: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | null>(null);

interface AuthProviderProps {
children: ReactNode;
}

export function AuthProvider({
children,
}: AuthProviderProps): React.ReactElement {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
isLoading: true,
user: null,
});

const restoreSession = async () => {
try {
setState((prev) => ({ ...prev, isLoading: true }));
const result = await mobileAuth.restoreSession();

if (result) {
setState({
isAuthenticated: true,
isLoading: false,
user: result.user,
});
} else {
setState({
isAuthenticated: false,
isLoading: false,
user: null,
});
}
} catch (error) {
console.warn("Session restore failed:", error);
setState({
isAuthenticated: false,
isLoading: false,
user: null,
});
}
};

const login = async (credentials: {
email: string;
password: string;
rememberMe?: boolean;
}) => {
try {
setState((prev) => ({ ...prev, isLoading: true }));
const result = await mobileAuth.login(credentials);
setState({
isAuthenticated: true,
isLoading: false,
user: result.user,
});
} catch (error) {
setState((prev) => ({ ...prev, isLoading: false }));
throw error;
}
};

const loginWithBiometrics = async () => {
try {
setState((prev) => ({ ...prev, isLoading: true }));
const result = await mobileAuth.loginWithBiometrics();
setState({
isAuthenticated: true,
isLoading: false,
user: result.user,
});
} catch (error) {
setState((prev) => ({ ...prev, isLoading: false }));
throw error;
}
};

const logout = async () => {
try {
setState((prev) => ({ ...prev, isLoading: true }));
await mobileAuth.logout();
setState({
isAuthenticated: false,
isLoading: false,
user: null,
});
} catch (error) {
setState((prev) => ({ ...prev, isLoading: false }));
throw error;
}
};

// Restore session on mount
useEffect(() => {
restoreSession();
}, []);

return (
<AuthContext.Provider
value={{
...state,
login,
loginWithBiometrics,
logout,
restoreSession,
}}
>
{children}
</AuthContext.Provider>
);
}

export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
133 changes: 67 additions & 66 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { RootStackParamList } from './types';

// ── Lazily-imported screens ──────────────────────────────────────────────────
// Lazy imports prevent each screen's bundle from blocking the initial render.
const HomeScreen = React.lazy(() => import('../pages/mobile/MobileLogin'));
const SettingsScreen = React.lazy(() => import('../pages/mobile/Settings'));
const PaymentHistoryScreen = React.lazy(
() => import('../pages/mobile/PaymentHistory'),
);

// Heavy feature screens are also kept lazy so their JS chunk is only loaded
// when the user actually navigates there.
const MobileCourseViewer = React.lazy(
() => import('../components/mobile/MobileCourseViewer'),
);
const MobileQuizManager = React.lazy(
() => import('../components/mobile/MobileQuizManager'),
);
import { NavigationContainer } from "@react-navigation/native";
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from "@react-navigation/native-stack";
import React from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import CourseViewerScreen from "../screens/CourseViewerScreen";
import HomeScreen from "../screens/HomeScreen";
import ProfileScreen from "../screens/ProfileScreen";
import QuizScreen from "../screens/QuizScreen";
import SearchScreen from "../screens/SearchScreen";
import SettingsScreen from "../screens/SettingsScreen";
import { AuthGuard } from "./AuthGuard";
import { RootStackParamList } from "./types";

// ── Stack navigator typed against RootStackParamList ────────────────────────
/**
Expand All @@ -28,54 +23,60 @@ const MobileQuizManager = React.lazy(
*/
const Stack = createNativeStackNavigator<RootStackParamList>();

/**
* AppNavigator
*
* The root stack navigator for TeachLink Mobile. All screens are registered
* here with their typed `name` prop; param shapes are inferred directly from
* `RootStackParamList` — no `as any` casts are needed anywhere in the tree.
*
* This component is intentionally *not* wrapped in `NavigationContainer`; that
* responsibility belongs to the Expo Router layout (`app/_layout.tsx`) which
* already provides a container via the `<Stack>` primitive. AppNavigator is
* designed for use inside a React Navigation `NavigationContainer` when the app
* is used outside of Expo Router (e.g., tests or standalone Native CLI builds).
*/
export default function AppNavigator() {
// Auth-guarded profile screen - receives navigation props from React Navigation
function ProtectedProfileScreen(
props: NativeStackScreenProps<RootStackParamList, "Profile">,
) {
return (
<Stack.Navigator
initialRouteName="Home"
screenOptions={{ headerShown: false }}
>
{/* ── Main screens ──────────────────────────────────────────────── */}
<Stack.Screen name="Home" component={HomeScreen as React.ComponentType} />

<Stack.Screen
name="Settings"
component={SettingsScreen as React.ComponentType}
/>
<AuthGuard>
<ProfileScreen route={props.route} navigation={props.navigation} />
</AuthGuard>
);
}

{/* ── Feature screens ───────────────────────────────────────────── */}
{/*
* MobileCourseViewer receives a typed `navigation` prop via the
* NativeStackNavigationProp<RootStackParamList, 'CourseViewer'> alias
* defined in src/navigation/types.ts. No `any` needed.
*/}
<Stack.Screen
name="CourseViewer"
component={MobileCourseViewer as React.ComponentType}
options={{ gestureEnabled: true }}
/>
// Auth-guarded settings screen
function ProtectedSettingsScreen(
props: NativeStackScreenProps<RootStackParamList, "Settings">,
) {
return (
<AuthGuard>
<SettingsScreen />
</AuthGuard>
);
}

{/*
* MobileQuizManager similarly receives a typed navigation prop via the
* QuizNavigationProp alias — see src/navigation/types.ts.
*/}
<Stack.Screen
name="Quiz"
component={MobileQuizManager as React.ComponentType}
options={{ gestureEnabled: true }}
/>
</Stack.Navigator>
export default function AppNavigator() {
return (
<NavigationContainer>
<SafeAreaView style={{ flex: 1 }}>
<Stack.Navigator
initialRouteName="Home"
screenOptions={{ headerShown: false }}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "TeachLink" }}
/>
<Stack.Screen
name="Search"
component={SearchScreen}
options={{ title: "Search" }}
/>
<Stack.Screen name="Profile" component={ProtectedProfileScreen} />
<Stack.Screen name="Settings" component={ProtectedSettingsScreen} />
<Stack.Screen
name="CourseViewer"
component={CourseViewerScreen}
options={{ title: "Course", headerShown: false }}
/>
<Stack.Screen
name="Quiz"
component={QuizScreen}
options={{ title: "Quiz", headerShown: false }}
/>
</Stack.Navigator>
</SafeAreaView>
</NavigationContainer>
);
}
Loading
Loading