diff --git a/apps/mobile/components/AppBootstrap.tsx b/apps/mobile/components/AppBootstrap.tsx new file mode 100644 index 0000000..81c70a4 --- /dev/null +++ b/apps/mobile/components/AppBootstrap.tsx @@ -0,0 +1,51 @@ +import { crypto } from '@chat-app/shared'; +import { type ReactNode, useEffect, useState } from 'react'; + +import { createLibsodiumBackend } from '../lib/cryptoBackend'; +import { BootError } from './BootError'; +import { BootSplash } from './BootSplash'; + +interface Props { + children: ReactNode; +} + +// React Native exposes ErrorUtils on the global. The types ship with RN but +// we cast defensively because the renderer used by Vitest does not. +interface RNErrorUtils { + getGlobalHandler: () => (err: Error, isFatal?: boolean) => void; + setGlobalHandler: (handler: (err: Error, isFatal?: boolean) => void) => void; +} + +// Initialises the crypto backend inside a useEffect (not at module-eval) so +// any failure surfaces in the React tree. Also installs a global JS error +// handler that routes unhandled throws to BootError; this catches errors +// thrown during render (e.g. the lazy env proxy reading a missing var) that +// would otherwise escape every per-screen ErrorBoundary. +export function AppBootstrap({ children }: Props) { + const [error, setError] = useState(null); + const [ready, setReady] = useState(false); + + useEffect(() => { + try { + crypto.setCryptoBackend(createLibsodiumBackend()); + setReady(true); + } catch (e: unknown) { + setError(e instanceof Error ? e : new Error(String(e))); + } + }, []); + + useEffect(() => { + const eu = (globalThis as unknown as { ErrorUtils?: RNErrorUtils }).ErrorUtils; + if (!eu) return; + const prev = eu.getGlobalHandler(); + eu.setGlobalHandler((err, isFatal) => { + prev?.(err, isFatal); + setError(err); + }); + return () => eu.setGlobalHandler(prev); + }, []); + + if (error) return ; + if (!ready) return ; + return <>{children}; +}