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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,40 @@ 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 { requireEnvVariables } from './src/config/env';
import { initializeLogging } from './src/config/logging';
import { AuthProvider, useAdaptiveTheme } 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 apiClient from './src/services/api';
import { requestQueue } from './src/services/api/requestQueue';
import { crashReportingService } from './src/services/crashReporting';
import { mobileAuthService } from './src/services/mobileAuth';
import {
addNotificationReceivedListener,
getLastNotificationResponse,
registerForPushNotifications,
registerTokenWithBackend,
removeNotificationListener,
} from './src/services/pushNotifications';
import { requestQueue } from './src/services/requestQueue';
import { initializeSecureStorage } from './src/services/secureStorage';
import socketService from './src/services/socket';
import syncService from './src/services/syncService';
import { useAppStore } from './src/store';
import { useNotificationStore } from './src/store/notificationStore';
import { handleCacheVersionUpdate } from './src/utils/cacheVersioning';
import { requireEnvVariables } from './src/utils/env';
import { appLogger } from './src/utils/logger';
import { handleNotificationReceived } from './src/utils/notificationHandlers';

// 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();

Expand Down Expand Up @@ -98,8 +98,9 @@ const App = () => {
crashReportingService.init();

// Initialize secure storage (Keychain/Keystore) for encrypted token storage
initializeSecureStorage().catch((error) => {
logger.error('Failed to initialize secure storage:', error);
initializeSecureStorage().catch((error: unknown) => {
const err = error instanceof Error ? error : new Error(String(error));
appLogger.errorSync('Failed to initialize secure storage', err);
// Continue app startup even if secure storage init fails
// (user will be prompted to re-authenticate if needed)
});
Expand All @@ -121,7 +122,7 @@ const App = () => {
socketService.connect();

// Initialize push notifications: request permissions and get device token
registerForPushNotifications().then(async (token) => {
registerForPushNotifications().then(async (token: string | null) => {
if (token) {
const { setPushToken, setTokenRegistered } = useNotificationStore.getState();
setPushToken(token);
Expand Down Expand Up @@ -241,4 +242,4 @@ const App = () => {
);
};

export default SHOW_STORYBOOK ? StorybookUI : App;
export default App;
40 changes: 30 additions & 10 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useDeepLink } from '../src/hooks/useDeepLink';
import { sessionRestorationService } from '../src/services/sessionRestoration';
import { preloadService } from '../src/services/preloadService';
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';

// Kick off resource hints early
Expand Down Expand Up @@ -72,15 +72,12 @@ const ThemeSync = () => {
const RootLayout = () => {
const router = useRouter();

const handleDeepLink = useCallback(
deepLink => {
const path = getPathFromDeepLink(deepLink);
if (path) {
router.replace(path);
}
},
[router]
);
const handleDeepLink = useCallback((deepLink: ParsedDeepLink) => {
const path = getPathFromDeepLink(deepLink);
if (path) {
router.replace(path);
}
}, [router]);

useDeepLink(handleDeepLink);

Expand Down Expand Up @@ -138,4 +135,27 @@ const RootLayout = () => {
<AnalyticsProvider>
<ScreenTracker />
<ThemeSync />
<OfflineIndicatorProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="course-viewer" options={{ headerShown: false }} />
<Stack.Screen name="profile/[userId]" options={{ headerShown: false }} />
<Stack.Screen name="search" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false }} />
<Stack.Screen name="qr-scanner" options={{ headerShown: false }} />
<Stack.Screen name="quiz" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
{/* DEV-only memory profiler overlay; renders null in production. */}
<MemoryProfilerOverlay />
</GestureHandlerRootView>
</OfflineIndicatorProvider>
</AnalyticsProvider>
</RetryErrorBoundary>
</ErrorBoundary>
);
};

export default RootLayout;

8 changes: 5 additions & 3 deletions components/themed-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ export type ThemedTextProps = TextProps & {
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
};

export function ThemedText({
export const ThemedText = ({
style,
lightColor,
darkColor,
type = 'default',
...rest
}: ThemedTextProps) {
}: ThemedTextProps) => {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
const { scale } = useDynamicFontSize();

Expand All @@ -35,10 +35,12 @@ export function ThemedText({
};

const variantStyle = getVariantStyle();
const variantLineHeight =
'lineHeight' in variantStyle ? variantStyle.lineHeight : undefined;
const scaledStyle = {
...variantStyle,
fontSize: scale(variantStyle.fontSize || 16),
lineHeight: variantStyle.lineHeight ? scale(variantStyle.lineHeight) : undefined,
lineHeight: variantLineHeight ? scale(variantLineHeight) : undefined,
};

return <Text style={[{ color }, scaledStyle, style]} allowFontScaling={false} {...rest} />;
Expand Down
80 changes: 80 additions & 0 deletions docs/typescript-strict-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# TypeScript Strict Mode

`tsconfig.json` sets `"strict": true`. The codebase must type-check with
`npx tsc --noEmit` exiting with code `0`, and CI enforces this via the
`Typecheck` step in `.github/workflows/ci.yml`.

## What `strict: true` enables

| Flag | Catches |
|-----------------------------------|---------------------------------------------------------------|
| `strictNullChecks` | Implicit `null` / `undefined` access on values that may be missing |
| `noImplicitAny` | Parameters / variables without explicit or inferable types |
| `strictFunctionTypes` | Function-parameter variance mismatches at assignment sites |
| `strictBindCallApply` | Wrong argument types on `.bind` / `.call` / `.apply` |
| `strictPropertyInitialization` | Class fields declared but not definitely assigned |
| `noImplicitThis` | Implicit `any` on `this` inside standalone functions |
| `alwaysStrict` | Emits `"use strict"` and parses files in strict mode |
| `useUnknownInCatchVariables` | Forces `catch (error)` bindings to be typed as `unknown` |

## Rules for contributors

- [ ] Never use `any` to silence a type error — use `unknown` and a type guard.
- [ ] Never use `// @ts-ignore` or `// @ts-expect-error`; fix the underlying type instead.
- [ ] Always treat `catch (error)` as `unknown` and narrow with
`error instanceof Error` before reading `.message` / `.stack`.
- [ ] Initialise class properties in the constructor, or use the `!` definite-assignment
operator only when the field is genuinely set elsewhere — add a comment with the
reason when you do.
- [ ] Prefer optional chaining (`?.`) and nullish coalescing (`??`) over non-null
assertions (`!`) for nullable values.
- [ ] When a third-party value's type is unknown to you, type it as `unknown` and
narrow it with a type guard rather than reaching for `any`.

### Idiomatic patterns

**Catch clauses** (matches the codebase convention, e.g.
`src/services/mobilePayments.ts`):

```ts
try {
await doWork();
} catch (error) {
appLogger.errorSync(
'[Module] doWork failed',
error instanceof Error ? error : new Error(String(error)),
);
}
```

**Nullable chains** (e.g. `src/config/env.ts`):

```ts
const value = process.env.SOME_KEY;
if (!value) {
throw new Error(`SOME_KEY is not set`);
}
return value;
```

**Discriminated unions over `as any` casts**: prefer narrowing through a
checked field rather than casting:

```ts
function isError(x: unknown): x is Error {
return x instanceof Error;
}
```

## Verifying locally

```bash
npx tsc --noEmit # must exit with code 0
npm test # must pass; new tests should also pass
```

## CI enforcement

`.github/workflows/ci.yml` runs `npx tsc --noEmit` on every push and PR via the
`Typecheck` job, followed by `npm test`. A PR that introduces a strict-mode
violation will fail CI before it can be merged.
Loading
Loading