Selective public-hoist patterns weren't enough — RN's index.js imports
invariant (and nullthrows, pretty-format, regenerator-runtime, and many
more) as bare transitive deps. Each fresh EAS build was hitting a
different missing-module error.
shamefully-hoist=true is Expo's official monorepo workaround: pnpm
mirrors npm/yarn's flat layout at the workspace root, so every package's
deps are reachable via standard Node resolution. Desktop (Vite-bundled)
is unaffected.
PackageList.java generated by RN autolinking imported expo.core.ExpoModulesPackage
(old SDK <48 name). Root cause: RN CLI couldn't read expo's react-native.config.js
through the pnpm symlink (it lives at .pnpm/expo@52..../expo/react-native.config.js),
so it fell back to deriving the import path from expo's Android gradle namespace
("expo.core") + assumed class name (ExpoModulesPackage), producing a path that
doesn't exist (the actual class lives at expo.modules.ExpoModulesPackage).
Added "expo" to .npmrc public-hoist-pattern so RN's autolinking can resolve
expo's react-native.config.js via the standard node_modules lookup. That config
explicitly sets packageImportPath to the correct expo.modules.ExpoModulesPackage.
Also corrected @expo/metro-runtime from 55.0.11 (added blindly earlier) to ~4.0.1
which is the SDK-52-aligned version.
pnpm's default strict isolation hides @babel/runtime/helpers/* and other
transitive deps from Metro, which expects flat node_modules layout.
Each missing dep was a separate failed EAS build (metro-runtime,
@babel/runtime, ...) — added .npmrc with public-hoist-pattern entries
that catch the common RN/Expo families so we stop whack-a-moling them.
Also kept @babel/runtime as direct dep of @chat-app/mobile for safety.
expo-router@4 imports @expo/metro-runtime as the very first thing in its
entry-classic.js so Fast Refresh works; bundler errored without it.
Also dropped expo-router/babel from babel.config.js — its functionality
moved into babel-preset-expo in SDK 50.
Phase 0 installed expo-application/expo-image-picker/expo-dev-client at
^55.x assuming the major version matched the SDK number. It doesn't —
each expo-* package has its own major. Misaligned versions pulled in
expo-modules-core@2.2.3 which expects an API that SDK 52's gradle plugin
host doesn't provide, breaking the Gradle build with
'expo-module-gradle-plugin not found'.
`expo install --fix` aligned everything to SDK 52 baselines:
expo-application 55.0.15 -> ~6.0.2
expo-dev-client 55.0.33 -> ~5.0.20
expo-image-picker 55.0.20 -> ~16.0.6
expo-sqlite 15.0.6 -> ~15.1.4
react-native 0.76.0 -> 0.76.9
+ RN companion libs (gesture-handler, screens, async-storage)
The 2500ms timer that drifts CallState from `ended` back to `idle` was
an inline magic number. Promote to a module-level constant with a
comment explaining why the value isn't arbitrary — picked from the
post-Phase-3 quality review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase-3 implementer ASCII'd four strings to side-step a Windows
console encoding issue during the apply step. The TypeScript / Metro
toolchain handles non-ASCII string literals cleanly — the workaround
was unnecessary and produced ugly UI labels ("Anruf lauft", "Hoerer",
"Verbinde ..."). Restore the originals.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks adding voice calls (1:1 + group) to the mobile app over the
same LiveKit + Supabase signaling stack the desktop uses:
1. @livekit/react-native + @livekit/react-native-webrtc deps, mic
permission strings, audio background mode, LiveKit Expo plugin.
2. callSignal.ts subscribe/publish helpers over Supabase realtime.
3. callContext.tsx state machine (idle/outgoing/incoming/connecting/
connected/ended) + LiveKit room lifecycle + audio routing.
4. IncomingCallModal at root with Annehmen/Ablehnen.
5. Mount CallProvider + global IncomingCallModal in _layout.tsx.
6. Register /call full-screen modal route.
7. In-call screen with participants list + mute/speaker/hangup.
8. Phone-icon header button on conversation detail + push to /call
on connect.
9. Workspace typecheck pass.
Out of scope: video, CallKit / ConnectionService native UI, VoIP push
wake-up, screen sharing, call history. Those are Phase 3.5 / 4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small follow-ups from the post-Phase-1 quality review:
* authContext.tsx — drop the dead `userId` extraction + the `void
userId` suppressor that masked an unused-locals warning. The session
is already implicitly threaded through the supabase client, so no
consumer of ensureDevice needed the value.
* authContext.tsx — switch the device-name string concat to a
template literal for consistency with the rest of the codebase.
* ErrorBoundary.tsx — replace the four inline hex literals with their
`theme/colors.ts` constants. The boundary was authored in Phase 0
before the theme module existed; this brings it in line with every
Phase 1 screen.
* apps/mobile/README.md — drop the stale Phase-0 paragraph about the
`lib/sharedSmoke.ts` canary (deleted in Phase 1) and add a short
pointer to the env-var setup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>