Optical air-gap file transfer companion for meow-decoder. Scans animated QR code GIFs from any screen using your phone camera and exports the captured frame data as a structured JSON file for desktop decryption.
No network. No cloud. No traces.
| Version | Download | Notes |
|---|---|---|
| v3.2.2 (latest) | β¬ Download APK | Bug fixes: capture init + camera guard |
| v3.2.0 | Download APK | β |
Sideload instructions:
- On your Android device: Settings β Apps β Special app access β Install unknown apps β allow your browser.
- Tap the download link above on your phone.
- Open the downloaded
.apkand tap Install. - Launch Meow Capture and grant camera permission when prompted.
- On your desktop, run
meow-encode(or open the web demo) and display the QR code on screen. - In the app, tap Scan Request QR or Import Capture Request (JSON) to begin.
Google Play Store listing coming soon β sideloading is the only install method for now.
The iOS version is in active development. Apple App Store listing coming soon. iOS users can scan QR codes today via the web demo in Safari.
| Platform | Status |
|---|---|
| Android sideload | β Available β see download links above |
| Google Play Store | π Coming soon |
| iOS (App Store) | π Coming soon |
v3.2 (2026) β Capture Quality Coach, Calibration Wizard (5-step preflight: camera, QR visibility, light, brightness, thermal), Settings screen (Strict / Convenience security mode), Diagnostics Panel (long-press version badge, safe-to-share diagnostics export), one-tap sanitized debug bundle export on Export screen, Export SHA-256 + filename copy, Request QR scanner, enriched session resume banner, decode-rate / duplicate-rate live metrics, VoiceOver milestone announcements, and
meow_decoder.mergemulti-device capture merge CLI. Video import hook is present but feature-flagged OFF (hidden from release UI). 274/274 tests, strict TypeScript, zero network permissions.v3.1 (2026) β Full accessibility + polish pass. Respects Reduce Motion system preference (SplashScreen, FrameOverlay, CatToast). VoiceOver/TalkBack announces toasts (
accessibilityLiveRegion).KeyboardAvoidingViewon Home; error banners announced; haptics on file load; stale errors cleared on re-focus. OnboardingScreen shows "Open Settings" recovery when camera permission is denied. Android hardware back button prompts confirmation before discarding an active capture session. 274/274 tests, strict TypeScript.v3 (2026) β Major UX hardening. SVG arc progress ring with fountain-threshold indicator. Adaptive frame-rate scanning (60 Hz β 10 Hz back-off). Stall detector toasts when no new frames arrive for 4 s. Pause / resume capture mid-session. Panic wipe via 3-second long-press cancel. Clipboard auto-wipe after export. Universal-link / deep-link support (
meow://capture?β¦). VoiceOver improvements on StabilityIndicator.v2 (2026) β Production-hardened release. Native VisionCamera v4 scanner, biometric export gate, FLAG_SECURE screenshot blocking, pinch-to-zoom + exposure control, haptic feedback, dynamic type, and full strict TypeScript coverage.
| Tool | Version |
|---|---|
| Node.js | β₯ 20 |
| React Native CLI | 0.73.4 |
| Xcode | β₯ 15 (iOS) |
| Android Studio | β₯ Iguana (Android) |
| Ruby | β₯ 3.0 (iOS CocoaPods) |
| Java | JDK 17+ (Android) |
macOS (iOS):
brew install cocoapodslibzbar (not required) β QR decoding is handled on-device by the OS (MLKit on Android, AVFoundation on iOS) via VisionCamera v4's native useCodeScanner. No additional system libraries needed.
# Install JS dependencies
cd mobile
npm install
# iOS
cd ios && pod install && cd ..
npx react-native run-ios
# Android
npx react-native run-androidβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Desktop (web demo or CLI encoder) β
β Opens wasm_browser_example.html β choose a mode: β
β Standard / FS / SchrΓΆdinger / Hybrid-PQ / Duress β
β β Displays QR code (static or animated GIF) on screen β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β Camera (optical, air-gapped)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MeowCapture (this app) β
β 1. Home β load capture request JSON (session params) β
β 2. Camera β aim at screen, app scans QR frames β
β β’ Single QR: immediately captured & complete β
β β’ Fountain GIF: scan until progress bar fills β
β 3. Export β save captured_frames.json to Downloads β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β USB/ADB transfer or manual copy
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Desktop (meow-decoder or web demo decrypt tab) β
β $ meow-decode-gif -i captured_frames.json -p "pass" β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
Open the web demo on the desktop (
examples/wasm_browser_example.htmlorweb_demo/wasm_browser_example_FULL.html). Choose any encryption mode: Standard, Forward Secrecy, SchrΓΆdinger, Post-Quantum, or Duress. -
Encrypt a file or message in the demo. For multi-frame (animated QR), the payload is too large for one code and will be fountain-coded automatically. For single-frame, a static QR appears.
-
Generate a capture request (or enter manually in the app):
- Multi-frame: set
expected_framesto the droplet count shown in the demo log. - Single-frame: set
expected_frames: 1.
# CLI alternative meow-encode --print-request -i file.pdf - Multi-frame: set
-
Load the request in the app β tap "Load JSON File" on the Home screen, or enter the session UUID and frame count manually.
-
Point your camera at the QR on screen. The app shows a Calibration Wizard first β a quick 5-step preflight that verifies camera permission, QR readability, light conditions, screen brightness, and device temperature. You can skip it if conditions are clearly good.
- Single-frame modes: app captures the QR and immediately completes.
- Fountain animated GIF: hold steady until the progress bar reaches 100%.
-
Confirm & Export β tap Confirm & Export on the Export screen. If Face ID / fingerprint is enrolled, biometric confirmation is required before any data is written to disk. Transfer
meow_capture_<session_id>.jsonback to the desktop via USB. -
Debug bundle (optional) β from the Export screen, tap Export Debug Bundle to generate a sanitized diagnostics file. This contains only metadata (app version, device info, capture stats, error history) β no payloads, passwords, or sensitive content. Safe to share for troubleshooting.
-
Decrypt β paste the captured JSON into the web demo's decrypt tab, or use the CLI:
meow-decode-gif -i meow_capture_<session_id>.json -p "password"
-
Multi-device merge (optional) β if multiple phones captured the same transfer:
# Merge two captures for maximum frame coverage before decoding python -m meow_decoder.merge \ --input capture-phone-a.json capture-phone-b.json \ --output merged.json # Then decode the merged file meow-decode-gif -i merged.json -p "password"
The merge tool deduplicates frame indices and recalculates the coverage ratio. All inputs must share the same
session_id; mismatched sessions are rejected.
The app recognises every QR format produced by the web demo (wasm_browser_example.html,
wasm_browser_example_FULL.html) and the Python CLI encoder.
| Format | Prefix | Source | Notes |
|---|---|---|---|
| Fountain | FOUNTAIN:<k>:<block_size>:<length>:<b64> |
Web demo animated QR / CLI | Each frame is a fountain droplet. Capture any ~67% of frames to decode. App auto-completes at ceil(expected_frames Γ 1.5). |
| Format | Prefix | Web Demo Mode | Notes |
|---|---|---|---|
| Standard | MEOW:<b64> |
Standard / Normal | AES-256-GCM password encryption. |
| Forward Secrecy | FS:<b64> |
Forward Secrecy | X25519 ephemeral key exchange (MEOW3). |
| SchrΓΆdinger | QUANTUM:<b64> |
SchrΓΆdinger | Dual-secret plausible deniability. |
| Post-Quantum Hybrid | HYBRID-PQ:<b64> |
Post-Quantum | ML-KEM-768/1024 + X25519 (MEOW5/MEOW4). |
| Duress | DURESS:<b64> |
Duress | Panic-password aware; reveals decoy on wrong key. |
| Format | Prefix | Notes |
|---|---|---|
| MEOW chunks | MEOW-N/total:<b64> |
Split MEOW: payload across N QR frames. Index = N-1. |
| Format | Shape | Notes |
|---|---|---|
| JSON | {"index": N, "data": "<b64>", "session_id"?: "..."} |
Used by meow-encode --mobile-bridge. |
Behaviour by format type:
- Single-frame (
MEOW:,FS:,QUANTUM:,HYBRID-PQ:,DURESS:): capture session auto-starts and auto-completes on the first valid scan. Setexpected_frames: 1in the capture request. - Fountain / multi-frame: camera stays active until fountain threshold is met
(
captured >= ceil(expected_frames Γ 1.5)). - Unknown prefixes (e.g. arbitrary QR codes in the environment) are silently ignored β only meow-format strings trigger the capture state machine.
The app validates all incoming requests with Zod strict schema. Extra fields are rejected.
{
"action": "capture",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"expected_frames": 45,
"timeout_seconds": 120
}| Field | Type | Required | Constraints |
|---|---|---|---|
action |
"capture" |
β | Must be exactly "capture" |
session_id |
UUID v4 | β | Validated as UUID regex |
expected_frames |
integer | β | 1 for single-frame modes; fountain frame count for multi-frame |
timeout_seconds |
integer | β | 1β600, defaults to 60 |
{
"schema_version": "1",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"captured_at": "2024-01-15T10:30:00.000Z",
"elapsed_ms": 45230,
"total_frames": 47,
"frames": [
{ "index": 0, "data": "FOUNTAIN:10:800:4523:AAB...", "timestamp_ms": 1705312200123 },
{ "index": 1, "data": "FOUNTAIN:10:800:4523:CCF...", "timestamp_ms": 1705312200223 }
]
}The data field contains the raw QR string including its format prefix:
| Capture mode | Example data value |
|---|---|
| Fountain multi-frame | FOUNTAIN:10:800:4523:AABg... |
| Standard | MEOW:AABg... |
| Forward Secrecy | FS:AABg... |
| SchrΓΆdinger | QUANTUM:AABg... |
| Post-Quantum | HYBRID-PQ:AABg... |
| Duress | DURESS:AABg... |
| Legacy chunked | MEOW-1/5:AABg... |
| CLI bridge | {"index":0,"data":"..."} (stored as-is) |
The desktop decoder identifies the encryption mode from the prefix and dispatches accordingly.
| Permission | Platform | Why |
|---|---|---|
CAMERA |
Android + iOS | Scan QR codes from screen β no images ever stored or transmitted |
WRITE_EXTERNAL_STORAGE |
Android β€ 9 | Write export JSON to Downloads folder |
VIBRATE |
Android | Haptic feedback: progress ticks, milestone pops, export outcome |
USE_BIOMETRIC + USE_FINGERPRINT |
Android | Biometric export gate (falls back gracefully if not enrolled) |
Explicitly not requested: INTERNET, RECORD_AUDIO, ACCESS_FINE_LOCATION, READ_CONTACTS, READ_EXTERNAL_STORAGE, BLUETOOTH, or any permission not in the table above.
The INTERNET permission is deliberately absent from AndroidManifest.xml β the OS enforces zero network access at the platform level, not just in application code.
| Control | Implementation |
|---|---|
| Zero network | No INTERNET permission β OS-enforced, not application-level |
| Screenshot blocking | FLAG_SECURE in MainActivity.onCreate β blocks screenshots, screen recording, and task-switcher thumbnail on Android |
| iOS task-switcher defense | isBackgrounding renders a solid privacy overlay when applicationWillResignActive fires, before the OS captures its snapshot |
| Biometric export gate | react-native-biometrics prompts Face ID / fingerprint / PIN before writing any data to disk |
| Memory wipe on background | AppState listener dispatches RESET (clears all frames from React state) on background or inactive |
| Foreground recovery | On returning from background the app navigates to Home β the wiped session cannot be resumed |
| Panic wipe | 3-second long-press on Cancel triggers immediate RESET + navigation to Home β no confirmation needed |
| Android back guard | Hardware back during CAPTURING / AWAITING_GIF / PAUSED triggers a confirmation alert before discarding frames |
| Clipboard wipe | Export JSON string is removed from the clipboard 45 s after sharing |
| Explicit export only | ExportScreen shows a confirmation card; no auto-export on mount |
| Input validation | Every capture request validated with Zod .strict() schema; extra fields and malformed UUIDs rejected |
| No decryption on device | Frame data stored as opaque base64 strings; all crypto operations happen on the desktop |
| No image retention | VisionCamera useCodeScanner passes only decoded string values to JS β camera frames never reach app memory |
audio={false} |
Microphone disabled on the <Camera> component |
The capture screen exposes live controls for real-world scanning conditions:
| Control | How to use | Purpose |
|---|---|---|
| Pinch to zoom | Standard two-finger pinch | Move further from screen; range 1Γ β 6Γ (capped to preserve decode quality) |
| Exposure β / + | Tap βοΈβ or βοΈ+ buttons | β2 β¦ +2 in 0.5 steps; reduce glare from bright screens or boost dim displays |
| Torch | Tap π‘ button | Illuminates surroundings in low light (hardware torch required) |
| Pause / Resume | Tap βΈ / |
Freeze scanning without losing captured frames; resumes from same state |
| Stop | Tap βΉ button | Finalises early β exports whatever frames have been collected |
| Panic wipe | Long-press Cancel for 3 s | Instantly wipes all captured frames and navigates Home β for high-pressure situations |
| Stability indicator | Automatic | Accelerometer warns when device motion may cause motion blur |
| Stall detector | Automatic | Toasts after 4 s with no new frames β prompts you to adjust camera position |
# Install JS dependencies
npm install
# Run Jest unit tests (274 tests)
npm test
# TypeScript strict type check (zero errors)
npm run type-check
# Lint (zero warnings)
npm run lint
# Auto-format
npm run format
# Bump version across all sources (package.json, config.ts, Info.plist)
npm run bump-version 3.3.0
# iOS β install CocoaPods then launch
npm run pod-install
npx react-native run-ios
# Android
npx react-native run-androidmobile/
βββ src/
β βββ types/
β β βββ capture.ts # CaptureRequest, CaptureResponse, CapturedFrame, ExportResult
β β βββ navigation.ts # Typed screen props for React Navigation
β β βββ declarations.d.ts # Ambient module types (react-native-biometrics)
β βββ constants/
β β βββ config.ts # FOUNTAIN_OVERHEAD, milestone thresholds, FPS, dedup timing
β β βββ theme.ts # Palette, PixelRatio-scaled typography, spacing, shadows
β βββ utils/ # base64 validation, formatters (pure, fully tested)
β βββ services/
β β βββ requestValidator.ts # Zod .strict() schema + safeValidateRequest
β β βββ qrDecoder.ts # Prefix-based format detection, payload parsing
β β βββ frameCollector.ts # Dedup, fountain threshold tracking
β β βββ jsonExporter.ts # RNFS write, chunked export, QR fallback chunks
β β βββ debugBundleExporter.ts # Sanitized debug bundle (no payloads/passwords)
β βββ hooks/
β β βββ useCapture.ts # useReducer state machine (IDLEβAWAITING_GIFβCAPTURINGβPAUSEDβCOMPLETE)
β β βββ useQRScanner.ts # VisionCamera v4 useCodeScanner (MLKit / AVFoundation); adaptive FPS
β β βββ useStabilityMonitor.ts # Accelerometer-based shake detection
β β βββ useStallDetector.ts # Detects 4 s+ periods with no new frames; fires toast callback
β β βββ useSessionManager.ts # Orchestrates capture + scanner + stability
β β βββ useSecureScreen.ts # isBackgrounding flag for iOS privacy overlay
β βββ components/
β β βββ CameraPreview.tsx # AnimatedCamera, pinch zoom, exposure bias, torch, privacy overlay
β β βββ ProgressHUD.tsx # SVG arc ring with fountain-threshold indicator
β β βββ FrameOverlay.tsx # Scan corners, status badges (AWAITING/CAPTURING/PAUSED/COMPLETE), reduce-motion-aware scan line
β β βββ CatWhiskerHUD.tsx # Whisker-style motion feedback
β β βββ CaptureCoachPanel.tsx # Live coaching hints (distance, brightness, shake)
β β βββ CalibrationWizard.tsx # 5-step preflight (camera, QR, light, brightness, thermal)
β β βββ DiagnosticsPanel.tsx # Hidden dev diagnostics (long-press version badge)
β β βββ StabilityIndicator.tsx # Shake magnitude bar
β β βββ CatToast.tsx # Queued slide-up toasts with accessibilityLiveRegion
β βββ screens/
β β βββ SplashScreen.tsx # Cat-eye animation; respects Reduce Motion system preference
β β βββ OnboardingScreen.tsx # First-run camera permission with Settings recovery on denial
β β βββ HomeScreen.tsx # Load capture request (file picker or manual entry); KeyboardAvoidingView
β β βββ CaptureScreen.tsx # Live camera, haptics, pause/resume, panic wipe, Android back guard
β β βββ ExportScreen.tsx # Biometric-gated confirm β JSON export or QR fallback
β βββ navigation/
β β βββ AppNavigator.tsx # NativeStack; gesture disabled on CaptureScreen
β βββ App.tsx # GestureHandlerRootView, MeowDarkTheme, CatToastProvider
βββ __tests__/ # Jest unit tests β pure logic, no render tests
βββ __mocks__/ # Native module mocks (VisionCamera, biometrics, RNFS, MMKV, sensors)
βββ android/
β βββ app/src/main/
β βββ AndroidManifest.xml # CAMERA + VIBRATE + USE_BIOMETRIC; NO INTERNET
β βββ java/β¦/MainActivity.kt # FLAG_SECURE in onCreate
βββ ios/ # NSCameraUsageDescription only in Info.plist
| Symptom | Likely cause | Fix |
|---|---|---|
| Blank camera preview | Permission denied | Settings β Apps β MeowCapture β Permissions β Camera |
| "Open Settings" button shown on Onboarding | Camera permission denied at OS level | Tap it β leads directly to the app's permission page |
| Frames not incrementing | Camera too far | Move 20β40 cm from screen |
| Low frame count warning | Motion blur | Use stability indicator; hold phone still |
| "No new frames" toast | Stall detected | Shift camera position slightly or adjust exposure |
| App completes instantly (0 frames shown) | expected_frames=0 in request JSON | Set expected_frames to the frame count from the demo log |
| Single-frame mode never auto-completes | expected_frames > 1 | Set expected_frames: 1 for static MEOW:/FS:/etc. QR codes |
| Export silently fails | Downloads folder full | Free storage and retry |
| Timeout before completion | GIF too fast / poor lighting | Increase timeout_seconds; use βοΈβ / βοΈ+ to compensate for glare |
| "Invalid request" error | Extra fields in request JSON | Remove unrecognised fields; see schema above |
| Non-meow QR silently ignored | Unknown prefix in environment | App only collects meow-prefixed codes; others are dropped |
| Biometric prompt never shows | No biometric enrolled on device | Falls back to unguarded export button automatically |
| Export button not visible | Still on confirm card | Tap Confirm & Export (or Export to Downloads if no biometrics) |
| Glare making QR unreadable | Bright laptop screen | Tap βοΈβ to reduce exposure bias; tilt phone slightly off-axis |
| Android back button dismisses capture | Expected β guarded | A confirmation dialog appears before frames are discarded |
If the picker doesn't show the file, pull it directly:
adb pull /sdcard/Download/meow_capture_<session_id>.json ./Applies to multi-frame fountain coded QRs (FOUNTAIN: prefix) only.
Single-frame modes (MEOW:, FS:, QUANTUM:, HYBRID-PQ:, DURESS:) complete in one scan.
For fountain-coded animated GIFs, the app uses a 1.5Γ redundancy factor. Capture completes automatically when:
captured_frames β₯ ceil(expected_frames Γ 1.5)
This means you can expect successful decryption even with ~33% frame loss due to:
- Camera motion blur during scanning
- Screen refresh timing mismatches
- QR scan failures on low-contrast frames
| Package | Version | Role |
|---|---|---|
react-native-vision-camera |
^4.0.0 | Native QR scanning via useCodeScanner (MLKit / AVFoundation) |
react-native-biometrics |
^3.0.1 | Biometric export gate (Face ID, fingerprint, device PIN fallback) |
react-native-reanimated |
^3.6.2 | UI-thread pinch zoom via useAnimatedProps |
react-native-gesture-handler |
^2.14.1 | Gesture.Pinch() for zoom, swipe-back guard on CaptureScreen |
react-native-haptic-feedback |
^2.2.0 | Progress ticks, milestone pops, export success/error |
react-native-sensors |
^7.3.6 | Accelerometer for stability monitor |
react-native-mmkv |
^2.12.2 | Synchronous first-launch flag (settings only β never frame data) |
react-native-fs |
^2.20.0 | Write export JSON to Downloads folder |
zod |
^3.22.4 | Strict capture-request schema validation |
Removed in v2: vision-camera-code-scanner (abandoned, used a fragile worklet require() hack, broken on Android 14+ / iOS 17+) and react-native-worklets-core (no longer needed for QR scanning).
Added in v3: @react-navigation/native deep-link support (universal links meow://capture?β¦). No new runtime dependencies β v3 features are built from existing packages (react-native-reanimated SVG arc, react-native-haptic-feedback, built-in BackHandler/Linking APIs).
CC BY-NC-SA 4.0 β see ../LICENSE