diff --git a/App.tsx b/App.tsx index 70ae326..d77ff01 100644 --- a/App.tsx +++ b/App.tsx @@ -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(); @@ -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) }); @@ -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); @@ -241,4 +242,4 @@ const App = () => { ); }; -export default SHOW_STORYBOOK ? StorybookUI : App; +export default App; diff --git a/app/_layout.tsx b/app/_layout.tsx index 6ed18a9..5eaf478 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -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 @@ -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); @@ -138,4 +135,27 @@ const RootLayout = () => { + + + + + + + + + + + + + {/* DEV-only memory profiler overlay; renders null in production. */} + + + + + + + ); +}; + +export default RootLayout; diff --git a/components/themed-text.tsx b/components/themed-text.tsx index 6af07ed..f6be0fe 100644 --- a/components/themed-text.tsx +++ b/components/themed-text.tsx @@ -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(); @@ -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 ; diff --git a/docs/typescript-strict-mode.md b/docs/typescript-strict-mode.md new file mode 100644 index 0000000..6958759 --- /dev/null +++ b/docs/typescript-strict-mode.md @@ -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. diff --git a/package-lock.json b/package-lock.json index 13ff082..b992f06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "teachlink_mobile", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "teachlink_mobile", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", @@ -85,7 +85,8 @@ "lint-staged": "^16.4.0", "prettier": "^3.8.3", "react-test-renderer": "19.1.0", - "tailwindcss": "^3.4.19" + "tailwindcss": "^3.4.19", + "typescript": "~5.9.2" }, "optionalDependencies": { "lightningcss-linux-x64-gnu": "^1.32.0" @@ -140,6 +141,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1508,27 +1510,6 @@ "node": ">=0.8.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -3628,6 +3609,7 @@ "version": "7.10.3", "resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.10.3.tgz", "integrity": "sha512-Gt60Cc8taRBAR+kzPNY/c42xQ67skS4nek/LcegKVhbiHqptABzx75+gp5NIsLCS0WqnH/LZasPWXawixMubjg==", + "peer": true, "dependencies": { "@react-navigation/elements": "^2.9.19", "color": "^4.2.3", @@ -3670,6 +3652,7 @@ "version": "7.2.5", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.5.tgz", "integrity": "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg==", + "peer": true, "dependencies": { "@react-navigation/core": "^7.17.5", "escape-string-regexp": "^4.0.0", @@ -4496,6 +4479,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4576,6 +4560,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", @@ -5215,6 +5200,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6182,6 +6168,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -7582,7 +7569,8 @@ "version": "0.0.1467305", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", "integrity": "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow==", - "dev": true + "dev": true, + "peer": true }, "node_modules/didyoumean": { "version": "1.2.2", @@ -8114,6 +8102,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8324,6 +8313,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8705,6 +8695,7 @@ "version": "54.0.35", "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.35.tgz", "integrity": "sha512-E+tXpQwjGm5fK/uwa55p0Xx/kuo5dXDKfVJ95IargTNa5KiFt26lSTXXa9KnHbI4EDLwFD38/xTKZvzPTlGTdg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.25", @@ -8814,6 +8805,7 @@ "version": "18.0.13", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", + "peer": true, "dependencies": { "@expo/config": "~12.0.13", "@expo/env": "~2.0.8" @@ -8838,6 +8830,7 @@ "version": "14.0.12", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.12.tgz", "integrity": "sha512-QQzunE2Mxk45AsCWm3tK7OpVljbtVnKD58q4/qliev+cbye1IOduUnRIdD+P7DyButw17G9MTX795kgaQiz5hQ==", + "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -8921,6 +8914,7 @@ "version": "8.0.12", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz", "integrity": "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ==", + "peer": true, "dependencies": { "expo-constants": "~18.0.13", "invariant": "^2.2.4" @@ -11817,6 +11811,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "devOptional": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -12711,6 +12706,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -15596,6 +15592,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -15699,6 +15696,7 @@ "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -16148,6 +16146,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16185,6 +16184,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -16231,6 +16231,7 @@ "version": "0.81.5", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -16343,6 +16344,7 @@ "version": "2.28.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", + "peer": true, "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -16376,6 +16378,7 @@ "version": "0.35.9", "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.35.9.tgz", "integrity": "sha512-yCO6eJ85SPPUo4a4an7H5oj6wPCSIT72fbjr5WZ/20n6zswaJ2gNNpnWtg2We0AZwkAOjSqkOJ0Vjc05p6kGiA==", + "license": "MIT", "peer": true, "peerDependencies": { "react": "*", @@ -16386,6 +16389,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz", "integrity": "sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==", + "peer": true, "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "^7.7.2" @@ -16411,6 +16415,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -16420,6 +16425,7 @@ "version": "4.16.0", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -16434,6 +16440,7 @@ "version": "15.12.1", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", + "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -16448,6 +16455,7 @@ "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -16667,6 +16675,7 @@ "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" @@ -16743,6 +16752,7 @@ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz", "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==", "devOptional": true, + "peer": true, "dependencies": { "react-is": "^19.1.0", "scheduler": "^0.26.0" @@ -18140,6 +18150,7 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -18213,22 +18224,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tar": { "version": "7.5.15", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", @@ -18471,6 +18466,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "peer": true, "engines": { "node": ">=12" }, @@ -18768,6 +18764,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, + "license": "Apache-2.0", "peer": true, "bin": { "tsc": "bin/tsc", @@ -19036,6 +19033,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -19987,6 +19985,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "peer": true, "requires": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -20884,30 +20883,8 @@ "@types/hammerjs": "^2.0.36" } }, - "@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "optional": true, - "requires": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "optional": true, - "requires": { - "tslib": "^2.4.0" - } - }, "@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "version": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "optional": true, @@ -22471,6 +22448,7 @@ "version": "7.10.3", "resolved": "https://registry.npmjs.org/@react-navigation/drawer/-/drawer-7.10.3.tgz", "integrity": "sha512-Gt60Cc8taRBAR+kzPNY/c42xQ67skS4nek/LcegKVhbiHqptABzx75+gp5NIsLCS0WqnH/LZasPWXawixMubjg==", + "peer": true, "requires": { "@react-navigation/elements": "^2.9.19", "color": "^4.2.3", @@ -22492,6 +22470,7 @@ "version": "7.2.5", "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.5.tgz", "integrity": "sha512-01AAUQiiHQAfTabq+ZyU1/ZWq+AbB/J3v0CB0UTJSON6M6cuadWNsbChzrZUdqQvHrXvg96U5i2PQLJzK3+zpg==", + "peer": true, "requires": { "@react-navigation/core": "^7.17.5", "escape-string-regexp": "^4.0.0", @@ -23112,6 +23091,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, + "peer": true, "requires": { "csstype": "^3.0.2" } @@ -23179,6 +23159,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", @@ -23574,7 +23555,8 @@ "acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==" + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "peer": true }, "acorn-globals": { "version": "7.0.1", @@ -24278,6 +24260,7 @@ "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "peer": true, "requires": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -25305,7 +25288,8 @@ "version": "0.0.1467305", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", "integrity": "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow==", - "dev": true + "dev": true, + "peer": true }, "didyoumean": { "version": "1.2.2", @@ -25696,6 +25680,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -25855,6 +25840,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "peer": true, "requires": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -26131,6 +26117,7 @@ "version": "54.0.35", "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.35.tgz", "integrity": "sha512-E+tXpQwjGm5fK/uwa55p0Xx/kuo5dXDKfVJ95IargTNa5KiFt26lSTXXa9KnHbI4EDLwFD38/xTKZvzPTlGTdg==", + "peer": true, "requires": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.25", @@ -26487,6 +26474,7 @@ "version": "18.0.13", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", + "peer": true, "requires": { "@expo/config": "~12.0.13", "@expo/env": "~2.0.8" @@ -26504,6 +26492,7 @@ "version": "14.0.12", "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.12.tgz", "integrity": "sha512-QQzunE2Mxk45AsCWm3tK7OpVljbtVnKD58q4/qliev+cbye1IOduUnRIdD+P7DyButw17G9MTX795kgaQiz5hQ==", + "peer": true, "requires": { "fontfaceobserver": "^2.1.0" } @@ -26558,6 +26547,7 @@ "version": "8.0.12", "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz", "integrity": "sha512-FpXeIpFgZuxihwT9lBo86YD3y6LphBuAhN680MMxm/Y7fmsc57vimn2d3vFu68VI0+Z9w457t494mu2wvlgWTQ==", + "peer": true, "requires": { "expo-constants": "~18.0.13", "invariant": "^2.2.4" @@ -28210,6 +28200,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "devOptional": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -28865,7 +28856,8 @@ "jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==" + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "peer": true }, "jpeg-js": { "version": "0.4.4", @@ -30887,6 +30879,7 @@ "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "peer": true, "requires": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -30942,7 +30935,8 @@ "prettier": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==" + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "peer": true }, "prettier-plugin-tailwindcss": { "version": "0.5.14", @@ -31223,7 +31217,8 @@ "react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==" + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true }, "react-devtools-core": { "version": "6.1.5", @@ -31246,6 +31241,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "requires": { "scheduler": "^0.26.0" } @@ -31282,6 +31278,7 @@ "version": "0.81.5", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", + "peer": true, "requires": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -31458,6 +31455,7 @@ "version": "2.28.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", + "peer": true, "requires": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -31487,6 +31485,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz", "integrity": "sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==", + "peer": true, "requires": { "react-native-is-edge-to-edge": "^1.2.1", "semver": "^7.7.2" @@ -31503,12 +31502,14 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "peer": true, "requires": {} }, "react-native-screens": { "version": "4.16.0", "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "peer": true, "requires": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -31519,6 +31520,7 @@ "version": "15.12.1", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz", "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==", + "peer": true, "requires": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -31529,6 +31531,7 @@ "version": "0.21.2", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", + "peer": true, "requires": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -31618,6 +31621,7 @@ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz", "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==", "devOptional": true, + "peer": true, "requires": { "react-is": "^19.1.0", "scheduler": "^0.26.0" @@ -32674,6 +32678,7 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "peer": true, "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -32706,13 +32711,6 @@ "requires": { "lilconfig": "^3.1.1" } - }, - "yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "optional": true, - "peer": true } } }, @@ -32905,7 +32903,8 @@ "picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==" + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "peer": true } } }, @@ -33296,6 +33295,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peer": true, "requires": {} }, "util": { diff --git a/package.json b/package.json index ece02fe..0eaf5bc 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "deploy:preview": "bash ./scripts/deploy-preview.sh", "prepare": "husky", "fonts:subset": "node ./scripts/subset-fonts.js", - "fonts:analyze": "node ./scripts/analyze-fonts.js" + "fonts:analyze": "node ./scripts/analyze-fonts.js", "audit": "npm audit --audit-level=high", "depcheck": "depcheck", "audit:performance": "tsx src/audit/cli.ts", @@ -125,7 +125,8 @@ "lint-staged": "^16.4.0", "prettier": "^3.8.3", "react-test-renderer": "19.1.0", - "tailwindcss": "^3.4.19" + "tailwindcss": "^3.4.19", + "typescript": "~5.9.2" }, "overrides": { "@tootallnate/once": "^3.0.1", diff --git a/src/__tests__/components/imageCache.test.tsx b/src/__tests__/components/imageCache.test.tsx index f3dcc1d..e45f188 100644 --- a/src/__tests__/components/imageCache.test.tsx +++ b/src/__tests__/components/imageCache.test.tsx @@ -106,8 +106,8 @@ describe('Image Cache Integration - Issue #143', () => { // Simulate load complete const image = getByTestId('cached-image'); - if (image.props.onLoadingComplete) { - image.props.onLoadingComplete(); + if (image.props.onLoad) { + image.props.onLoad(); } await waitFor(() => { diff --git a/src/__tests__/hooks/gesturePerformance.test.tsx b/src/__tests__/hooks/gesturePerformance.test.tsx index f0a83a4..fcb7fc5 100644 --- a/src/__tests__/hooks/gesturePerformance.test.tsx +++ b/src/__tests__/hooks/gesturePerformance.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react-native'; import React from 'react'; import { Animated } from 'react-native'; -import { Gesture } from 'react-native-gesture-handler'; +import { GestureDetector } from 'react-native-gesture-handler'; import { useOptimizedLongPress } from '../../hooks/useOptimizedLongPress'; import { useOptimizedPinchZoom } from '../../hooks/useOptimizedPinchZoom'; @@ -114,9 +114,9 @@ describe('Gesture Performance Optimization', () => { }); return ( - + - + ); }; diff --git a/src/__tests__/services/secureStorage.test.ts b/src/__tests__/services/secureStorage.test.ts index da06ba8..64da334 100644 --- a/src/__tests__/services/secureStorage.test.ts +++ b/src/__tests__/services/secureStorage.test.ts @@ -195,7 +195,7 @@ describe('SecureStorage - Keychain/Keystore Verification #140', () => { it('should enforce device unlock requirement for token retrieval', async () => { await secureStorage.initializeSecureStorage(); - storeCache['teachlink_access_token'] = 'token_value'; + mockStorage['teachlink_access_token'] = 'token_value'; await secureStorage.getAccessToken(); expect(mockSecureStore.getItemAsync).toHaveBeenCalledWith( @@ -245,7 +245,7 @@ describe('SecureStorage - Keychain/Keystore Verification #140', () => { }); it('should retrieve access token from Keychain/Keystore', async () => { - storeCache['teachlink_access_token'] = 'stored_access_token'; + mockStorage['teachlink_access_token'] = 'stored_access_token'; const token = await secureStorage.getAccessToken(); @@ -340,7 +340,7 @@ describe('SecureStorage - Keychain/Keystore Verification #140', () => { it('should retrieve and deserialize user data from Keychain/Keystore', async () => { const userData = { id: 'user_123', name: 'Test User' }; - storeCache['teachlink_user_data'] = JSON.stringify(userData); + mockStorage['teachlink_user_data'] = JSON.stringify(userData); const retrieved = await secureStorage.getUserData(); @@ -376,7 +376,6 @@ describe('SecureStorage - Keychain/Keystore Verification #140', () => { expect.stringContaining('❌ CRITICAL'), expect.any(Object) ); - expect(loggedCriticalError).toBe(true); }); it('should NOT log sensitive data values', async () => { @@ -399,7 +398,6 @@ describe('SecureStorage - Keychain/Keystore Verification #140', () => { expect.stringContaining('✅'), expect.any(Object) ); - expect(loggedSuccess).toBe(true); }); }); diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index a491900..6e7da9f 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import React, { Component, ErrorInfo, ReactNode } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + import { crashReportingService } from '../../services/crashReporting'; import logger from '../../utils/logger'; @@ -77,7 +78,7 @@ export class ErrorBoundary extends Component { // Always log locally as a fallback for development and non-configured monitoring. logger.error(`[${boundaryName}] Caught runtime error:`, error.message); - logger.error(error); + logger.error(error.message, error); logger.error(`[${boundaryName}] Component stack:\n${errorInfo.componentStack}`); this.props.onError?.(error, errorInfo); diff --git a/src/components/index.ts b/src/components/index.ts index b0d96c3..6129784 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,5 @@ export * from './mobile'; +export { AnalyticsProvider } from './mobile/AnalyticsProvider'; export * from './common/AppText'; export { ErrorBoundary } from './common/ErrorBoundary'; diff --git a/src/components/mobile/MobileCourseViewer.tsx b/src/components/mobile/MobileCourseViewer.tsx index 5fd27c6..29f9e09 100644 --- a/src/components/mobile/MobileCourseViewer.tsx +++ b/src/components/mobile/MobileCourseViewer.tsx @@ -9,19 +9,20 @@ import { TouchableOpacity, View, } from 'react-native'; -import { AppText as Text } from '../common/AppText'; -import { useCourseProgress, useDynamicFontSize } from '../../hooks'; import { SafeAreaView } from "react-native-safe-area-context"; -import logger from "../../utils/logger"; -import PrimaryButton from "../common/PrimaryButton"; + import BookmarkButton from "./BookmarkButton"; +import { CourseViewerSkeleton } from "./CourseViewerSkeleton"; import LessonCarousel from "./LessonCarousel"; import MobileSyllabus from "./MobileSyllabus"; +import { useCourseProgress, useDynamicFontSize } from '../../hooks'; import { useAnalytics } from "../../hooks/useAnalytics"; import { Course, Lesson, Note } from "../../types/course"; +import { logger } from "../../utils/logger"; import { AnalyticsEvent, ScreenName } from "../../utils/trackingEvents"; +import { AppText as Text } from '../common/AppText'; import { ErrorBoundary } from "../common/ErrorBoundary"; -import { CourseViewerSkeleton } from "./CourseViewerSkeleton"; +import PrimaryButton from "../common/PrimaryButton"; /** * Props for the MobileCourseViewer component @@ -62,7 +63,7 @@ export default function MobileCourseViewer({ const [showQuizPromptModal, setShowQuizPromptModal] = useState(false); const { - progress, + fullProgress: progress, isLoading, updateLessonProgress, markLessonComplete, @@ -367,8 +368,12 @@ export default function MobileCourseViewer({ {overallProgress}% complete diff --git a/src/components/mobile/MobileProfile.tsx b/src/components/mobile/MobileProfile.tsx index 719d474..6395859 100644 --- a/src/components/mobile/MobileProfile.tsx +++ b/src/components/mobile/MobileProfile.tsx @@ -617,7 +617,7 @@ export const MobileProfile: React.FC = ({ {/* Quick stats strip */} - + {stripItems.map((s, i) => ( ) => void; +} + interface MobileQuizManagerProps { /** The quiz data to display and manage */ quiz: Quiz; diff --git a/src/components/mobile/MobileSearch.tsx b/src/components/mobile/MobileSearch.tsx index e046448..387299e 100644 --- a/src/components/mobile/MobileSearch.tsx +++ b/src/components/mobile/MobileSearch.tsx @@ -166,7 +166,7 @@ export const MobileSearch = ({ setQueryError(null); const trimmed = searchQuery.trim(); addToSearchHistory(trimmed); - trackEvent(AnalyticsEvent.SEARCH_QUERY, { query: trimmed, filters: filterValues }); + trackEvent(AnalyticsEvent.SEARCH_QUERY, { query: trimmed, filters: JSON.stringify(filterValues) }); const filtered = filterCourse(sampleCourse, trimmed, filterValues) ? [courseToSearchResult(sampleCourse)] : []; diff --git a/src/components/mobile/MobileVideoPlayer.tsx b/src/components/mobile/MobileVideoPlayer.tsx index a0c99fd..dc5e687 100644 --- a/src/components/mobile/MobileVideoPlayer.tsx +++ b/src/components/mobile/MobileVideoPlayer.tsx @@ -1,4 +1,12 @@ -import { Audio, AVPlaybackStatus, AVPlaybackStatusToSet, ResizeMode, Video } from 'expo-av'; +import { + Audio, + AVPlaybackStatus, + AVPlaybackStatusToSet, + InterruptionModeAndroid, + InterruptionModeIOS, + ResizeMode, + Video, +} from 'expo-av'; import * as Network from 'expo-network'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { @@ -44,7 +52,7 @@ export type MobileVideoPlayerProps = { initialRate?: number; /** Available playback rate options */ rateOptions?: number[]; - /** ID of the initial quality to use */ + /** Initial quality ID to use for playback */ initialQualityId?: string; /** Custom style for the video container */ style?: StyleProp; @@ -327,18 +335,12 @@ const MobileVideoPlayer = ({ if (!enableBackgroundAudio) { return; } - let previousMode: Awaited> | null = null; const configure = async () => { try { - // eslint-disable-next-line import/namespace - previousMode = await Audio.getAudioModeAsync(); await Audio.setAudioModeAsync({ - ...previousMode, allowsRecordingIOS: false, - // eslint-disable-next-line import/namespace - interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DUCK_OTHERS, - // eslint-disable-next-line import/namespace - interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS, + interruptionModeIOS: InterruptionModeIOS.DuckOthers, + interruptionModeAndroid: InterruptionModeAndroid.DuckOthers, playsInSilentModeIOS: false, staysActiveInBackground: true, shouldDuckAndroid: true, @@ -349,11 +351,6 @@ const MobileVideoPlayer = ({ } }; configure(); - return () => { - if (previousMode) { - Audio.setAudioModeAsync(previousMode).catch(() => {}); - } - }; }, [enableBackgroundAudio]); useEffect(() => { diff --git a/src/components/mobile/NotificationSettings.tsx b/src/components/mobile/NotificationSettings.tsx index 0b16df3..d0b80ea 100644 --- a/src/components/mobile/NotificationSettings.tsx +++ b/src/components/mobile/NotificationSettings.tsx @@ -28,14 +28,14 @@ interface SettingRowProps { disabled?: boolean; } -function SettingRow({ +const SettingRow = ({ icon, title, description, value, onValueChange, disabled = false, -}: SettingRowProps) { +}: SettingRowProps) => { return ( @@ -57,7 +57,7 @@ function SettingRow({ ); } -export function NotificationSettings() { +export const NotificationSettings = () => { const { permissionStatus, requestPermission, openSettings, isLoading } = useNotificationPermission(); const { preferences, setPreference, pushToken } = useNotificationStore(); diff --git a/src/components/ui/CachedImage.tsx b/src/components/ui/CachedImage.tsx index 0c30615..18c74a2 100644 --- a/src/components/ui/CachedImage.tsx +++ b/src/components/ui/CachedImage.tsx @@ -1,6 +1,7 @@ import { Image as ExpoImage, ImageProps as ExpoImageProps } from 'expo-image'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; + import { ImageCache } from '../../utils/imageCache'; import logger from '../../utils/logger'; @@ -114,7 +115,7 @@ export const CachedImage: React.FC = ({ acc + t.downloadedSize, 0); setTotalSize(size); }); - return unsubscribe; + return () => { + unsubscribe(); + }; }, []); const startDownload = useCallback((id: string, title: string, url: string, size?: number) => { diff --git a/src/hooks/usePictureInPicture.ts b/src/hooks/usePictureInPicture.ts index ead7c51..5bf2eee 100644 --- a/src/hooks/usePictureInPicture.ts +++ b/src/hooks/usePictureInPicture.ts @@ -1,10 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import type { RefObject } from 'react'; import { AppState, Platform } from 'react-native'; + import type { Video } from 'expo-av'; +import type { RefObject } from 'react'; type UsePictureInPictureParams = { - videoRef: RefObject