Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .rnstorybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const preview: Preview = {
},
},
decorators: [
(Story) => (
Story => (
<SafeAreaProvider>
<AuthProvider>
<AnalyticsProvider>
Expand Down
2 changes: 1 addition & 1 deletion .rnstorybook/stories/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const meta = {
title: 'Example/Button',
component: Button,
decorators: [
(Story) => (
Story => (
<View style={{ flex: 1, alignItems: 'flex-start' }}>
<Story />
</View>
Expand Down
11 changes: 8 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Text,
View,
} from 'react-native';

import StorybookUI from './.rnstorybook';
import './global.css';
import { ErrorBoundary } from './src/components/common/ErrorBoundary';
Expand All @@ -25,7 +26,7 @@ import {
subscribeToCacheStatus,
} from './src/services/api';
import { warmCriticalCaches } from './src/services/cacheWarming';
import { crashReportingService } from './src/services/cashReporting';
import { crashReportingService } from './src/services/crashReporting';
import { featureCapabilities } from './src/services/featureCapabilities';
import { inAppReviewService } from './src/services/inAppReview';
import { mobileAuthService } from './src/services/mobileAuth';
Expand All @@ -37,12 +38,12 @@ import {
removeNotificationListener,
} from './src/services/pushNotifications';
import { requestQueue } from './src/services/requestQueue';
import { searchIndexService } from './src/services/searchIndex';
import { initializeSecureStorage } from './src/services/secureStorage'; // Added missing storage helper mock path
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 { searchIndexService } from './src/services/searchIndex';
import { handleCacheVersionUpdate } from './src/utils/cacheVersioning';
import { requireEnvVariables } from './src/utils/env';
import { appLogger } from './src/utils/logger';
Expand Down Expand Up @@ -217,6 +218,11 @@ const App = () => {
}
});

// Notification listener subscription
const subscription = addNotificationReceivedListener(notification =>
handleNotificationReceived(notification)
);

// ===== DEFERRED PATH — runs after user interactions complete =====
// These tasks are non-critical: they enhance the experience but are not
// needed for the initial render or core feature set. Scheduling them
Expand Down Expand Up @@ -278,7 +284,6 @@ const App = () => {
return () => {
socketService.disconnect();
syncService.stopAutoSync();
notificationCleanup();
removeNotificationListener(subscription);
// @ts-ignore
global.onunhandledrejection = undefined;
Expand Down
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ import { retrieveLogFiles, clearLogFiles } from '@/config/logging';

// Get all stored logs
const logs = await retrieveLogFiles();
logs.forEach((log) => console.log(log));
logs.forEach(log => console.log(log));

// Clear log storage
await clearLogFiles();
Expand All @@ -165,7 +165,6 @@ Copy `.env.example` to `.env` and set the following:
| `EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS` | No | Enable push notifications (`true` / `false`) |
| `EXPO_PUBLIC_STORYBOOK` | No | Enable Storybook mode (`true` / `false`) |


The app validates `EXPO_PUBLIC_API_BASE_URL` and `EXPO_PUBLIC_SOCKET_URL` at startup and will refuse to launch with invalid or missing values.

For EAS builds, secrets are configured per build profile in `eas.json` rather than `.env`.
Expand Down Expand Up @@ -229,19 +228,18 @@ Run `eas credentials` to set up or repair iOS/Android signing credentials.

The project defines three EAS build profiles in `eas.json`:

| Profile | Description | Usage |
|---|---|---|
| **development** | Fast internal build for debugging, includes development client and runs on the `development` channel. | `eas build --profile development` |
| **preview** | Internal preview build, generates an APK for Android, runs on the `preview` channel. | `eas build --profile preview` |
| **production** | Production‑ready build with auto‑incremented version numbers for iOS and Android, publishes to the `production` channel. | `eas build --profile production` |
| Profile | Description | Usage |
| --------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------- |
| **development** | Fast internal build for debugging, includes development client and runs on the `development` channel. | `eas build --profile development` |
| **preview** | Internal preview build, generates an APK for Android, runs on the `preview` channel. | `eas build --profile preview` |
| **production** | Production‑ready build with auto‑incremented version numbers for iOS and Android, publishes to the `production` channel. | `eas build --profile production` |

These profiles can also be used when submitting:

- `eas submit --profile production` will use the production credentials defined in the `submit.production` section of `eas.json`.

Refer to the official EAS docs for more details.


---

## 🚀 Deployment
Expand Down Expand Up @@ -285,4 +283,4 @@ EXPO_PUBLIC_ENABLE_PUSH_NOTIFICATIONS=true

> ⚠️ Never commit your `.env` file. It is listed in `.gitignore`.

See [DEPLOY.md](./DEPLOY.md) for platform-specific setup (Google Play & App Store), build profiles, troubleshooting, and security notes.
See [DEPLOY.md](./DEPLOY.md) for platform-specific setup (Google Play & App Store), build profiles, troubleshooting, and security notes.
28 changes: 20 additions & 8 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useTheme } from '@/store';

const HomeTabIcon = ({ color }: { color: string }) => (
<IconSymbol size={28} name="house.fill" color={color} />
);

const SearchTabIcon = ({ color }: { color: string }) => (
<IconSymbol size={28} name="magnifyingglass" color={color} />
);

const ProfileTabIcon = ({ color }: { color: string }) => (
<IconSymbol size={28} name="person.fill" color={color} />
);

const DashboardTabIcon = ({ color }: { color: string }) => (
<IconSymbol size={28} name="chart.bar.fill" color={color} />
);

const TabLayout = () => {
const theme = useTheme();

Expand All @@ -25,32 +41,28 @@ const TabLayout = () => {
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
tabBarIcon: HomeTabIcon,
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'Search',
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="magnifyingglass" color={color} />
),
tabBarIcon: SearchTabIcon,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />,
tabBarIcon: ProfileTabIcon,
}}
/>
<Tabs.Screen
name="dashboard"
options={{
title: 'Dashboard',
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="chart.bar.fill" color={color} />
),
tabBarIcon: DashboardTabIcon,
}}
/>
</Tabs>
Expand Down
4 changes: 2 additions & 2 deletions app/(tabs)/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ScreenName } from '@/utils/trackingEvents';

const LazyTeamDashboard = createLazyRoute({
importFn: () =>
import('@/components/mobile/TeamDashboard/TeamDashboard').then((m) => ({
import('@/components/mobile/TeamDashboard/TeamDashboard').then(m => ({
default: m.TeamDashboard,
})),
LoadingFallback: DashboardSkeleton,
Expand All @@ -23,7 +23,7 @@ const LazyTeamDashboard = createLazyRoute({

const DashboardTab = () => {
const { trackScreen } = useAnalytics();
const theme = useAppStore((s) => s.theme);
const theme = useAppStore(s => s.theme);

useEffect(() => {
trackScreen(ScreenName.HOME, { tab: 'dashboard' });
Expand Down
4 changes: 2 additions & 2 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ScreenName } from '@/utils/trackingEvents';

const LazyHomeScreenContent = createLazyRoute({
importFn: () =>
import('@/screens/HomeScreenContent').then((m) => ({ default: m.HomeScreenContent })),
import('@/screens/HomeScreenContent').then(m => ({ default: m.HomeScreenContent })),
LoadingFallback: HomeScreenSkeleton,
boundaryName: 'HomeRoute',
});
Expand Down Expand Up @@ -51,7 +51,7 @@ const HomeScreen = () => {
useEffect(() => {
const cleanup = fetchHomeData();
return cleanup;
// eslint-disable-next-line react-hooks/exhaustive-deps -- simulated home fetch runs once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps -- simulated home fetch runs once on mount
}, []);

if (isLoading) {
Expand Down
10 changes: 4 additions & 6 deletions app/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { createLazyRoute } from '@/utils/lazyRoute';

const LazyMobileProfile = createLazyRoute({
importFn: () =>
import('@/components/mobile/MobileProfile').then((m) => ({ default: m.MobileProfile })),
import('@/components/mobile/MobileProfile').then(m => ({ default: m.MobileProfile })),
LoadingFallback: ProfileSkeleton,
boundaryName: 'ProfileTabRoute',
});

const ProfileTab = () => {
const theme = useAppStore((s) => s.theme);
const user = useAppStore((s) => s.user);
const theme = useAppStore(s => s.theme);
const user = useAppStore(s => s.user);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
Expand All @@ -23,9 +23,7 @@ const ProfileTab = () => {

const userId = user?.id ?? '123';

return (
<LazyMobileProfile userId={userId} isDark={theme === 'dark'} isLoading={isLoading} />
);
return <LazyMobileProfile userId={userId} isDark={theme === 'dark'} isLoading={isLoading} />;
};

export default ProfileTab;
11 changes: 3 additions & 8 deletions app/(tabs)/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ import { useRouter } from 'expo-router';
import { useEffect, useState } from 'react';
import { Alert, StyleSheet, View } from 'react-native';

import {
CourseCardSkeleton,
SearchResultItem,
SearchScreenSkeleton,
Skeleton,
} from '@/components';
import { CourseCardSkeleton, SearchResultItem, SearchScreenSkeleton, Skeleton } from '@/components';
import { sampleCourse } from '@/data/sampleCourse';
import { createLazyRoute } from '@/utils/lazyRoute';

const LazyMobileSearch = createLazyRoute({
importFn: () =>
import('@/components/mobile/MobileSearch').then((m) => ({ default: m.MobileSearch })),
import('@/components/mobile/MobileSearch').then(m => ({ default: m.MobileSearch })),
LoadingFallback: SearchScreenSkeleton,
boundaryName: 'SearchRoute',
});
Expand Down Expand Up @@ -46,7 +41,7 @@ const SearchTab = () => {
useEffect(() => {
const cleanup = fetchSearchData();
return cleanup;
// eslint-disable-next-line react-hooks/exhaustive-deps -- simulated search fetch runs once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps -- simulated search fetch runs once on mount
}, []);

const handleResultPress = (item: SearchResultItem) => {
Expand Down
22 changes: 11 additions & 11 deletions app/+html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ type RootHtmlProps = {
children: React.ReactNode;
};

export default function RootHtml({ children }: RootHtmlProps) {
return (
<html lang="en">
<head>
<ScrollViewStyleReset />
<style dangerouslySetInnerHTML={{ __html: CRITICAL_SPLASH_CSS }} />
</head>
<body>{children}</body>
</html>
);
}
const RootHtml = ({ children }: RootHtmlProps) => (
<html lang="en">
<head>
<ScrollViewStyleReset />
<style dangerouslySetInnerHTML={{ __html: CRITICAL_SPLASH_CSS }} />
</head>
<body>{children}</body>
</html>
);

export default RootHtml;
8 changes: 4 additions & 4 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import { CacheStatusOverlay, MemoryProfilerOverlay } from '../components/DevTool
import { RetryErrorBoundary } from '../components/ErrorBoundary/RetryErrorBoundary';
import '../global.css'; // NativeWind CSS
import { AnalyticsProvider, ErrorBoundary, OfflineIndicatorProvider } from '../src/components';
import AppLifecycleManager from '../src/components/AppLifecycleManager';
import { KeyboardDelegateProvider } from '../src/components/common/KeyboardDelegateProvider';
import { UpdateNotificationModal } from '../src/components/common/UpdateNotificationModal';
import { useAnalytics } from '../src/hooks';
import { useAppUpdate } from '../src/hooks/useAppUpdate';
import { useDeepLink } from '../src/hooks/useDeepLink';
import { preloadService } from '../src/services/preloadService';
import { sessionRestorationService } from '../src/services/sessionRestoration';
import { scrollPositionService } from '../src/services/scrollPositionService';
import { sessionRestorationService } from '../src/services/sessionRestoration';
import { useAppStore } from '../src/store';
import { getPathFromDeepLink } from '../src/utils/linkParser';
import { getPathFromDeepLink, type ParsedDeepLink } from '../src/utils/linkParser';
import { prefetchExternalResources } from '../src/utils/resourceHints';
import AppLifecycleManager from '../src/components/AppLifecycleManager';

// Kick off resource hints early
prefetchExternalResources();
Expand Down Expand Up @@ -100,7 +100,7 @@ const RootLayout = () => {
const router = useRouter();

const handleDeepLink = useCallback(
deepLink => {
(deepLink: ParsedDeepLink) => {
const path = getPathFromDeepLink(deepLink);
if (path) {
router.replace(path);
Expand Down
Loading