# CLAUDE.md — Warden ## Project Overview Android browser router. When a user taps an HTTP/HTTPS link in any app — or shares text containing one to Warden — this app intercepts it via Android intent filters and presents a picker so the user chooses which browser opens it, in what privacy mode, with which intent extras. Built with Expo (React Native). App display name: **Warden** (both launcher tile and in-app). App ID: `org.vikingware.webwarden` — kept as-is so existing installs receive updates. ## Tech Stack - **Framework**: Expo SDK ~54, React Native 0.81.5, React 19.1.0 - **Routing**: none. Single screen registered via `registerRootComponent`. - **Language**: TypeScript (strict mode) - **Key native module**: local `modules/default-browser/` (Kotlin) for intent dispatch, default-browser detection, shared-text intake - **Linking**: `expo-linking` for URL interception - **Linting**: eslint-config-expo (`npm run lint`) - **Path alias**: `@/*` maps to project root ## Architecture Single-file app — no router, no navigation library. ``` index.js — entry; calls registerRootComponent(App) App.tsx — wraps Home with SafeAreaProvider + StatusBar Home() does everything: state, URL/share intake, UI ``` ### URL Flow 1. `Home` mounts, calls `Linking.getInitialURL()` and our native `getInitialSharedText()` on cold start. 2. `Linking.addEventListener('url', ...)` catches subsequent VIEW intents while the app is running. 3. `AppState.addEventListener('change', ...)` polls the native shared-text getter on every foreground transition (catches new SEND intents). 4. The first valid `https?://` URL we see populates the URL field and marks the session as external. 5. User taps a browser → native `openInBrowser(url, pkg, mode)` fires the appropriate Intent. On success in external mode, `BackHandler.exitApp()` finishes our task so back-from-browser returns to the originating app. ### Key Config - **Android intent filters** (`app.json`): - VIEW + `http`/`https` + `BROWSABLE`/`DEFAULT`/`APP_BROWSER` — registers us as a browser candidate - SEND + `text/plain` — accepts shared text from any app's share sheet - **Android queries** (`app.json`): package list of every browser we support, so Android 11+ package-visibility lets us see them - **Android package**: `org.vikingware.webwarden` - **Release-only blocked permission**: `android.permission.INTERNET` is stripped from release builds via a small config plugin at `plugins/with-release-no-internet.js`. Debug builds keep INTERNET so Metro can reach the app. The plugin writes `android/app/src/release/AndroidManifest.xml` with a `tools:node="remove"` directive — do NOT set `blockedPermissions` in app.json for INTERNET, that would strip it from debug too ## Key Files | File | Purpose | |------|---------| | `App.tsx` | Entire app UI + state | | `index.js` | Registers App with React Native | | `constants/browsers.ts` | Browser list + private-mode strategy per browser | | `constants/theme.ts` | Palette, Fonts | | `constants/build-info.ts` | Auto-generated version + commit at release time | | `modules/default-browser/` | Local Kotlin Expo module | | `app.json` | Expo config, intent filters, Android queries | | `scripts/release-apk.sh` | Builds signed arm64 APK, tags, uploads to Gitea | ## Design System Dark forest theme. All colors from `Palette` in `constants/theme.ts`: - Background: `#0a1a12` (bg), surfaces in `#1a2e1a` - Text: `#f2ead3` (primary), `#a8b89a` (muted) - Accent: `#6b8e4e` / `#8fb87a` (bright) - Highlight: `#b8d49a` ## Commands ```bash npm start # Start Metro dev server npm run android # Build & install on emulator (expo run:android) npm run lint # ESLint npx expo prebuild # Regenerate android/ from app.json (required after intent filter changes) ``` ## Important Notes - After changing `app.json` (intent filters, queries, etc.), run `npx expo prebuild` to regenerate the `android/` directory - The `android/` directory is generated — do not manually edit `AndroidManifest.xml` or Gradle files; changes will be overwritten by prebuild - To add a new browser: add entry to `BROWSERS` array in `constants/browsers.ts` AND add the package name to `app.json` → `android.queries.package` - NDK version required: `27.1.12297006` - App is Android-only; web/iOS support intentionally removed for size - No router, no navigation lib, no animation lib — keeping the dep tree flat is the lean trick. Don't add `expo-router`, `react-navigation`, `react-native-reanimated`, etc. unless we genuinely need them. ## Release Conventions - **Releases repo**: `ssh://git@orly/home/git/webwarden-releases.git` (bare, owned by `git:git`). Every dev/prod release pushes an APK here under `releases//Warden-.apk` + `SHA256SUMS`. - **Obtainium**: points at `https://git.smesh.lol/webwarden`. The README on that page is rendered as HTML; Obtainium's HTML source scans `` tags for URLs ending in `.apk`, picks the last one (alphabetically by URL), and extracts the version from the filename using standard regex patterns. - **Only the latest release goes in the README**. Multiple `.apk` links cause Obtainium to pick the alphabetically-last URL, which may not be the actual latest (lexical sort ≠ semver). One link = no ambiguity. Older releases are available in the releases repo archive but are not linked from the README. - **Version tags**: use semver `vX.Y.Z` for stable releases (these sort approximately correctly). Dev releases use `vX.Y.Z-dev-` tags. The `release-dev.sh` script auto-updates the README's Releases section to show only the latest build. - **Do NOT put version hashes in filenames** as the primary sorting key. Obtainium extracts the version from the filename — keep it parseable by the standard regex (`[0-9]+.[0-9]+.[0-9]+` etc.). - **Side-loading**: `release-dev.sh` auto-installs to any connected device via `adb install -r`. Builds for `arm64-v8a` only.