A React Native (Expo) crime-evidence recorder for the Custos Diretriz platform. It captures video/photo, pins it to IPFS, and anchors the hash on-chain — reusing the existing dapp's hosted API and smart contracts.
Its flagship feature is the Hidden Guardian: triple-press the power button on Android to start a silent background recording — no preview, no need to open the app.
capture (in-app camera OR hidden service)
→ pin to IPFS (Pinata)
→ POST /api/guest-save on the hosted dapp (server signs as guest recorder)
→ evidence hash anchored on-chain (Celo / Starknet)
Anything that can't reach the network is stored locally and retried automatically on next launch (src/lib/pipeline.ts), mirroring the dapp's emergency-save behaviour. No funded wallet is needed on the device — the hosted backend signs guest records.
The Hidden Guardian (power-button trigger)
| Android | iOS | |
|---|---|---|
| Trigger | Power button ×3 | Volume button ×3 (Apple blocks power-button interception) |
| Records with screen off / app closed | ✅ background foreground-service | ❌ not allowed by iOS |
| Preview shown | None (hidden) | In-app camera opens (foreground) |
| Notification | Required by Android while recording | n/a |
Android (modules/hidden-recorder): a foreground Service (HiddenCameraService.kt) registers for SCREEN_ON/SCREEN_OFF broadcasts. Each power press toggles the screen once; 3 toggles within 2s fire a recording. It records via CameraX VideoCapture with no preview surface, saves an mp4 to app-private storage, and emits the file URI to JS, which runs the upload pipeline. Android law requires a persistent notification while a camera/mic foreground service runs — "hidden" means no camera preview, not an invisible process.
iOS (HiddenRecorderModule.swift): Apple does not allow apps to intercept the power/side button or record the camera in the background. The fallback is a rapid volume-button gesture (KVO on the audio session's output volume) that opens the in-app recorder.
When calling enableTrigger({ pressCount, windowMs, maxDurationMs }) (src/lib/hiddenRecorder.ts):
pressCount— presses to trigger (default 3)windowMs— max time for the gesture (default 2000)maxDurationMs— auto-stop after this long (default 60000)
cp .env.example .env # add EXPO_PUBLIC_PINATA_JWT (same as the dapp's NEXT_PUBLIC_IPFS_KEY)
npm installThe native Hidden Guardian module is not in Expo Go — you need a dev/production build:
# Android (device or emulator + Android SDK)
npx expo run:android
# iOS (macOS + Xcode)
npx expo run:ios
# or a cloud build with EAS
npx eas build --profile development --platform androidexpo run:* regenerates the native projects from app.json (the android/ and ios/ folders are git-ignored — this is Continuous Native Generation). The config plugin plugins/withHiddenRecorder.js injects the foreground <service> and its foregroundServiceType.
For the JS-only screens you can still use Expo Go (npx expo start), but the Guardian toggle will report "native build required".
src/app/ expo-router screens: index, record, evidence, settings
src/lib/
config.ts API base URL, Pinata JWT, IPFS gateway
chains.ts chain registry + CrimeRecord ABI (ported from the dapp)
ipfs.ts pin a local file to IPFS via Pinata
evidence.ts anchor on-chain via hosted /api/guest-save; read evidence
pipeline.ts secureEvidence() + offline retry queue + local history
hiddenRecorder.ts JS bridge to the native module (degrades safely)
modules/hidden-recorder/ local Expo native module (Kotlin + Swift)
plugins/withHiddenRecorder.js config plugin (Android manifest service)
Camera and microphone are requested in-app. On Android 13+ the OS also asks for notification permission (the foreground-service notification). Everything is declared in app.json.
Recording laws vary by jurisdiction — some require consent to record audio or in private spaces. Users are responsible for lawful use. On-chain anchoring makes evidence timestamped and tamper-evident; it is not legal advice.