feat(privacy+dist): telemetry consent gate + app store distribution prep (#4)

* fix(security): fail closed on biometric init error

H-03: setting isAuthenticated: true on initialization failure was a
security bypass — any crash during biometric setup granted full access.
Fail closed instead; user sees auth prompt on next open.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): use Crypto.randomUUID for connection IDs

H-04: Math.random() is not cryptographically random. Connection IDs are
used as SecureStore key suffixes; switch to expo-crypto randomUUID for
a secure source.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(deps): pin expo-crypto to ~15.0.9

15.0.10 does not exist on npm; ~15.0.9 is the latest stable in the 15.x series compatible with Expo SDK 54.

* feat: add OpenCode Connect coming-soon waitlist card

Adds a discoverable 'OpenCode Connect — Coming Soon' card to the
add-connection quick-connect screen. Users can enter their email and
tap 'Join Waitlist' to send a pre-filled mailto. No backend required.

* fix(cua): detect actual screen dimensions and fix JSON parsing

- Get real screen size via `wm size` instead of hardcoding 1080x2400;
  emulator is 1080x1920 so y-coordinates were systematically off
- Extract first JSON object via regex when model returns multiple objects
- Use AZURE_OPENAI_MODEL env var for deployment name (defaults gpt-5.4)
- Add AZURE_DEV_AI_* path for Azure AI Foundry endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(security): SHA-pin upload-google-play and sanitize notification bodies

M-02: Pin r0adkll/upload-google-play to commit SHA e738b9d (v1.1.5)
to prevent supply-chain hijack via tag mutation.

M-03: Sanitize all push notification bodies — strip control chars,
truncate to 200 chars. Prevents server-supplied strings (error messages,
file paths from permission patterns, session titles) from leaking
unbounded text into the OS notification drawer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(privacy): add telemetry consent gate for Sentry crash reporting

Sentry was always-on, violating F-Droid anti-feature policy and user
trust norms. Now gated behind explicit opt-in:

- First-launch consent modal (TelemetryConsentModal) shows once on
  fresh install; user can Allow or Decline.
- Consent state persisted in expo-secure-store (survives restarts).
- Settings > Privacy section: crash reporting toggle + privacy policy link.
- initSentry() called only after consent granted — not on app start.

Closes #3 (partial)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(config): add real icons and complete iOS/Android app.json config

- Add 1024×1024 app icon, 432×432 adaptive icon foreground, 200×200 splash
- iOS: push notification entitlement (aps-environment: production), speech/
  microphone/camera/photo usage descriptions for future features, disable
  ITSAppUsesNonExemptEncryption
- Android: adaptive icon with dark background (#0F172A), versionCode: 1
- expo-notifications plugin wired in app.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(dist): add iOS CI workflow, README rewrite, CONTRIBUTING, and LICENSE

- publish-app-store.yml: EAS Build + TestFlight submission; runs on tag/release/
  workflow_dispatch; bumps ios.buildNumber from github.run_number
- README: full rewrite — features, install badges, connection guide, contributing
- CONTRIBUTING.md: contribution guide for OSS contributors
- LICENSE: MIT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(dist): add store listings, strategy, privacy policy, F-Droid/IzzyOnDroid templates

- distribution/strategy.md: monetization strategy (free client + opencode Cloud)
- distribution/play-listing.md: Google Play store copy (name, description, tags)
- distribution/app-store-listing.md: App Store listing copy
- distribution/privacy-policy.{md,html}: GDPR-compliant privacy policy
- distribution/PLAY_CONSOLE_SETUP.md: Play Console setup runbook
- distribution/ios-enrollment-runbook.md: Apple Developer Program enrollment steps
- distribution/SIGNING-KEY-FINGERPRINTS.md: keystore fingerprint for reproducible builds
- distribution/fdroid-submission/: F-Droid metadata template
- distribution/izzyondroid-submission/: IzzyOnDroid submission template
- distribution/whatsnew/: Play Store release notes (en-US)
- distribution/whatsnew-ios/: TestFlight release notes
- distribution/play-graphics/: Play Store screenshot placeholders
- distribution/app-store-graphics/: App Store screenshot placeholders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(telemetry): handle SecureStore failure + Android back button

- add .catch() on loadTelemetryConsent() so SecureStore rejection
  shows the consent modal instead of blocking startup forever
- add onRequestClose={onDecline} to Modal so Android back button
  records the decline rather than silently dismissing
- fix catch block in telemetry.ts to not clobber _resolved when
  SecureStore read fails mid-session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): run gradlew clean to prevent stale modules.json duplicate

Sentry Gradle plugin writes modules.json to src/main/assets; cached
build intermediates contain an old copy → mergeReleaseAssets fails
with 'Duplicate resources'. Running clean before assembleRelease
clears the intermediate state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): remove android build output cache causing duplicate modules.json

Caching android/app/build/intermediates and android/app/.cxx causes
two issues:
1. Stale modules.json in intermediates → Duplicate resources error
2. .cxx CMake artifacts reference absolute paths → ninja clean fails

Keeping only Gradle distribution cache (~/.gradle) which is safe.
Expo prebuild regenerates android sources fresh each run anyway.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Den
2026-05-25 17:42:03 -07:00
committed by GitHub
parent 7ea41216c8
commit 2b9b571d6e
51 changed files with 3463 additions and 98 deletions

View File

@@ -0,0 +1,137 @@
# Reproducible Build Notes — ai.opencode.mobile
F-Droid's modern `AllowedAPKSigningKeys` path requires that F-Droid's build
server can compile the same APK and arrive at a binary that matches the
pre-signed APK we supply via GitHub releases. Any non-determinism in the build
will break this verification.
---
## Issues found (2026-05-24)
### 1. Kotlin error log files tracked in git — MEDIUM
**Files committed:**
```
android/.kotlin/errors/errors-1779181311003.log
android/.kotlin/errors/errors-1779181311094.log
```
**Problem:** These log files contain absolute host paths:
```
While analysing /home/azureuser/workspace/opencode-mobile/node_modules/...
```
When F-Droid builds from source on their server, these log files will not
exist (or will contain different paths). Since they are tracked in git and
checked out during the build, they could cause differing build outputs
if the Kotlin compiler reads or embeds them. More practically, they make
the source tree non-portable — a smell that will draw reviewer attention.
**Recommended fix:** Add `.kotlin/` to `android/.gitignore`:
```
# android/.gitignore (add this line)
.kotlin/
```
Then remove the tracked files:
```bash
git rm -r --cached android/.kotlin/
git commit -m "chore: untrack kotlin error log files from android/.kotlin/"
```
This is a trivial fix. Do it before filing the F-Droid MR.
---
### 2. android/ directory tracked in git — LOW (expected but notable)
`expo prebuild` regenerates `android/` from `app.json` and `package.json`.
F-Droid's build metadata uses `npx expo prebuild` as a `prebuild:` step,
which means F-Droid rebuilds `android/` from scratch on their server.
The tracked `android/app/build.gradle` and other generated files must match
what `expo prebuild` produces. If the Expo SDK version drifts between what is
committed and what npm installs, the build will fail.
**Mitigation already in place:** `package-lock.json` is committed, which pins
all npm dependency versions. The F-Droid metadata `Builds:` step uses
`npm install --legacy-peer-deps` which respects `package-lock.json`.
**Residual risk:** If `expo prebuild` is non-deterministic (e.g., writes the
current date/time into generated files), subsequent runs will produce different
outputs. This is unlikely but should be verified by running prebuild twice and
comparing outputs:
```bash
npx expo prebuild --platform android --non-interactive --clean
git diff android/
```
---
### 3. Hermes bytecode embedding — LOW
The React Native Hermes engine compiles the JavaScript bundle to Hermes bytecode
at build time. The bytecode format is versioned but should be deterministic for
the same JS source + Hermes version. The Hermes version is pinned via
`react-native` in `package-lock.json`, so this is low risk.
---
### 4. PNG crunching — LOW
`build.gradle` has `crunchPngs true` for release builds. PNG crunching via aapt2
is generally deterministic but can vary across aapt2 versions. F-Droid's build
environment may use a different Android build tools version.
**Mitigation:** Pin `buildToolsVersion` in `android/build.gradle` explicitly
rather than relying on the Expo-supplied default. Check via:
```bash
grep buildToolsVersion android/build.gradle android/app/build.gradle
```
---
### 5. No hardcoded timestamps found — PASS
Grepped `android/` for `System.currentTimeMillis`, `new Date()`, `buildTime`,
`BUILD_DATE`, `UUID.randomUUID()` — no results. This is the most common
reproducibility killer and is clean here.
---
### 6. No absolute host paths in build files — PASS
Grepped `android/` `*.gradle` and `*.properties` for `/home/`, `/Users/`,
`C:\` — no results in build config files.
---
## Priority action items before F-Droid MR
| Priority | Item | Effort |
|----------|------|--------|
| HIGH | Add `.kotlin/` to `android/.gitignore` and untrack log files | 5 min |
| MEDIUM | Run `expo prebuild` twice, compare output with `git diff` | 15 min |
| MEDIUM | Pin `buildToolsVersion` explicitly in `android/build.gradle` | 5 min |
| LOW | Verify Hermes bytecode is deterministic (compare two builds) | 30 min |
| LOW | Test full reproducible build using F-Droid's Docker build env | Hours |
---
## How to test reproducible builds
F-Droid provides a reproducible build test tool:
```bash
# Install fdroidserver
pip install fdroidserver
# Test reproducibility against a released APK
fdroid signatures path/to/app-release.apk
# Full build test
fdroid build ai.opencode.mobile:<versionCode> --verbose
```
See https://f-droid.org/en/docs/Reproducible_Builds/ for the full guide.

View File

@@ -0,0 +1,163 @@
# APK Size Optimization for F-Droid / IzzyOnDroid
## Current state
| Artifact | Size |
|----------|------|
| Universal AAB (`app-release.aab`) | 58.5 MB |
| Per-ABI APK (estimated) | ~20–25 MB |
| IzzyOnDroid per-APK limit | 30 MB |
`bundletool` is not installed in this environment, so per-ABI APK sizes were not
measured directly. The estimate above is based on typical Expo/Hermes React Native
apps:
- Hermes JS engine binary: ~8–10 MB per ABI
- React Native native libraries: ~5–8 MB per ABI
- JavaScript bundle (ABI-independent): ~6–8 MB
- Assets (icons, splash, etc.): ~2–3 MB
**Conclusion:** arm64-v8a APK is likely ~20–25 MB — within IzzyOnDroid's 30 MB limit.
No ABI splits required for the initial submission.
To verify when bundletool is available:
```bash
bundletool build-apks \
--bundle=android/app/build/outputs/bundle/release/app-release.aab \
--output=apks.apks \
--mode=universal
# For per-ABI sizes:
bundletool build-apks \
--bundle=android/app/build/outputs/bundle/release/app-release.aab \
--output=apks-splits.apks \
--mode=default
unzip apks-splits.apks -d apk-splits/
ls -lh apk-splits/splits/
```
---
## If arm64-v8a APK exceeds 30 MB — fix approach
### Option A: ABI splits in Expo config plugin (recommended)
`android/app/build.gradle` is regenerated by `npx expo prebuild` on every CI run,
so direct edits are overwritten. The correct approach is an Expo config plugin.
Create `plugins/withAbiSplits.js`:
```js
const { withAppBuildGradle } = require('@expo/config-plugins');
module.exports = function withAbiSplits(config) {
return withAppBuildGradle(config, (config) => {
const contents = config.modResults.contents;
// Insert splits block inside android { ... } after defaultConfig
if (!contents.includes('splits {')) {
config.modResults.contents = contents.replace(
/defaultConfig \{/,
`splits {
abi {
reset()
enable true
universalApk true
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
}
defaultConfig {`
);
}
return config;
});
};
```
Register in `app.json`:
```json
{
"expo": {
"plugins": [
"./plugins/withAbiSplits.js"
]
}
}
```
After `npx expo prebuild`, `android/app/build.gradle` will contain the splits block.
Run `./gradlew assembleRelease` — produces separate APKs per ABI.
For IzzyOnDroid / F-Droid: attach `app-arm64-v8a-release.apk` to the GitHub release.
### Option B: fdroid Gradle product flavor
Add a `fdroid` flavor that includes only arm64-v8a:
```groovy
// In the config plugin or directly in build.gradle (pre-prebuild override)
android {
flavorDimensions "distribution"
productFlavors {
fdroid {
dimension "distribution"
ndk {
abiFilters "arm64-v8a"
}
}
play {
dimension "distribution"
}
}
}
```
Build with: `./gradlew assembleFdroidRelease`
This also provides a clean hook for excluding FCM artifacts (see below).
---
## FCM / expo-notifications
`expo-notifications` compiles FCM receiver classes even when only local
notifications are used (the app only calls `scheduleNotificationAsync` — no
push token retrieval). F-Droid scanner may flag `com.google.firebase:firebase-messaging`
as `NonFreeNet` or `NonFreeDep`.
**Mitigation for F-Droid mainline:** add a `fdroid` product flavor that
excludes the FCM artifact:
```groovy
// In config plugin
android {
flavorDimensions "distribution"
productFlavors {
fdroid {
dimension "distribution"
}
}
}
configurations.fdroidImplementation {
exclude group: 'com.google.firebase', module: 'firebase-messaging'
}
```
Or patch `expo-notifications` `build.gradle` via a Gradle init script to
replace the FCM dependency with a no-op stub in the `fdroid` flavor.
**This is not required for IzzyOnDroid** (more tolerant of GMS dependencies
if they are not actively invoked). Track the F-Droid reviewer's feedback
before investing in this fix — they may accept without it given the
local-only usage.
---
## Action items (when ready to optimize)
1. Install bundletool: `sudo apt-get install bundletool` or download JAR from
https://github.com/google/bundletool/releases
2. Run `build-apks --mode=default` to measure per-ABI sizes
3. If arm64-v8a > 30 MB: implement config plugin (Option A above)
4. For F-Droid mainline FCM concern: implement `fdroid` product flavor (Option B)
5. Document measured sizes in this file once known

View File

@@ -0,0 +1,153 @@
# F-Droid Mainline Submission Checklist
This checklist covers the steps to file a Merge Request against
https://gitlab.com/fdroid/fdroiddata to add `ai.opencode.mobile` to the
F-Droid main repository.
**Do NOT start this process until ALL prerequisites are checked.**
---
## Prerequisites (must all be true before filing)
- [ ] First Google Play release is live (proves signing key is in production use)
- [ ] Signed APK (not AAB) is attached to a GitHub release tag (e.g. `v0.2.4`)
- [ ] Sentry opt-in gate is merged to `main` (avoids `Tracking` anti-feature)
- [ ] `expo-notifications` FCM-free flavor exists OR F-Droid team has been pre-warned
(see `SIZE-OPTIMIZATION.md` section "FCM Flavor")
- [ ] Signing key SHA-256 fingerprint is confirmed in `distribution/SIGNING-KEY-FINGERPRINTS.md`
- [ ] Reproducible build has been tested locally (see `REPRODUCIBLE-BUILD-NOTES.md`)
---
## Step 1 — Prepare the signing key fingerprint
```bash
# Get the colon-separated fingerprint
keytool -list -v \
-keystore keystores/production-release.jks \
-storepass <STOREPASS> \
| grep "SHA256:"
# Example output:
# SHA256: 0C:25:9D:94:E0:FF:EA:5D:63:19:61:4B:22:9D:4B:6B:DC:22:DE:1F:56:E3:8E:76:94:83:98:D2:DF:6A:A0:99
# Convert to lowercase without colons (AllowedAPKSigningKeys format):
# 0c259d94e0ffea5d6319614b229d4b6bdc22de1f56e38e769483 98d2df6aa099
```
Update `distribution/fdroid-submission/metadata.yml`:
- Replace `<SIGNING_KEY_SHA256_FINGERPRINT_LOWERCASE_NO_COLONS>` with the fingerprint
- Replace `<FIRST_GITHUB_RELEASE_TAG>` with the actual tag (e.g. `v0.2.4`)
---
## Step 2 — Fork fdroiddata
```bash
# On GitLab
# 1. Go to https://gitlab.com/fdroid/fdroiddata
# 2. Fork to your personal GitLab account (not org — fdroid prefers personal forks)
# 3. Clone locally:
git clone https://gitlab.com/<YOUR_GITLAB_USERNAME>/fdroiddata.git
cd fdroiddata
git remote add upstream https://gitlab.com/fdroid/fdroiddata.git
git fetch upstream
git checkout -b add-ai.opencode.mobile upstream/master
```
---
## Step 3 — Add the metadata file
```bash
cp /path/to/opencode-mobile/distribution/fdroid-submission/metadata.yml \
metadata/ai.opencode.mobile.yml
```
Verify:
- `metadata/ai.opencode.mobile.yml` exists
- `AllowedAPKSigningKeys` has the correct lowercase-no-colons fingerprint
- `commit:` points to a real tag in the GitHub repo
- `versionCode` and `versionName` match the APK attached to the release
---
## Step 4 — Test the build locally (optional but strongly recommended)
F-Droid provides a Docker-based build environment:
```bash
# Install fdroidserver
pip install fdroidserver
# Verify metadata parses cleanly
fdroid readmeta
# Attempt a build (requires Docker + significant time)
fdroid build ai.opencode.mobile:<versionCode>
```
If the build fails, fix `metadata/ai.opencode.mobile.yml` before filing the MR.
---
## Step 5 — File the Merge Request
```bash
git add metadata/ai.opencode.mobile.yml
git commit -m "Add ai.opencode.mobile (OpenCode Mobile)"
git push origin add-ai.opencode.mobile
```
Go to https://gitlab.com/<YOUR_GITLAB_USERNAME>/fdroiddata → open an MR
against `fdroid/fdroiddata:master`.
MR title: `Add ai.opencode.mobile`
MR description template:
```
## New app: OpenCode Mobile
**Package:** ai.opencode.mobile
**License:** MIT
**Category:** Development
**Source:** https://github.com/dzianisv/opencode-mobile
OpenCode Mobile is a free, open-source mobile client for the opencode AI
coding agent (sst/opencode). MIT licensed. Crash reporting opt-in default OFF.
Anti-features: NonFreeNet (user-self-hosted backend may connect to proprietary AI APIs).
Using AllowedAPKSigningKeys path — pre-signed APK from GitHub releases.
Build steps: npm install → expo prebuild → Gradle assembleRelease.
```
---
## Step 6 — Respond to reviewer feedback
- F-Droid maintainers typically review within 2–8 weeks.
- Monitor the MR for comments. Common asks:
- Build reproducibility evidence
- Clarification on anti-features
- Pinning build dependencies to exact versions
- Removing or stubbing FCM/GMS dependencies
---
## Step 7 — After acceptance
- F-Droid builds from source on their CI. First index update may take 1–2 weeks.
- Add `ai.opencode.mobile` to F-Droid's inclusion notice in `docs/fdroid.md`.
- Update `distribution/strategy.md` status row for F-Droid.
- Notify IzzyOnDroid via the inclusion issue that mainline accepted the app
(IzzyOnDroid will then auto-delist within their next index rebuild).
---
## Reference
- F-Droid inclusion criteria: https://f-droid.org/en/docs/Inclusion_Policy/
- F-Droid metadata format: https://f-droid.org/en/docs/Build_Metadata_Reference/
- AllowedAPKSigningKeys: https://f-droid.org/en/docs/Reproducible_Builds/
- fdroiddata: https://gitlab.com/fdroid/fdroiddata

View File

@@ -0,0 +1,107 @@
# F-Droid metadata for ai.opencode.mobile
# Target file path in fdroiddata: metadata/ai.opencode.mobile.yml
#
# BEFORE FILING THE MR:
# 1. Replace <SIGNING_KEY_SHA256_FINGERPRINT> with the actual colon-separated hex fingerprint
# (found in distribution/SIGNING-KEY-FINGERPRINTS.md)
# 2. Replace <FIRST_GITHUB_RELEASE_TAG> with the first tag that has a signed APK attached
# (e.g. v0.2.4)
# 3. Verify the build steps produce a matching APK against AllowedAPKSigningKeys
#
# Do NOT file this MR until:
# - First Google Play release is live (establishes signing key in production use)
# - Sentry opt-in gate is merged to main (anti-feature Tracking avoided)
# - You have the exact colon-separated SHA-256 key fingerprint confirmed
Categories:
- Development
License: MIT
AuthorName: VIBE TECHNOLOGIES, LLC
AuthorEmail: support@vibebrowser.app
WebSite: https://opencode.vibebrowser.app
SourceCode: https://github.com/dzianisv/opencode-mobile
IssueTracker: https://github.com/dzianisv/opencode-mobile/issues
Changelog: https://github.com/dzianisv/opencode-mobile/releases
Summary: Drive your self-hosted AI coding agent from your phone
Description: |-
OpenCode Mobile is a free, open-source (MIT) client for the opencode AI coding
agent (sst/opencode). Connect to your self-hosted opencode server and drive
AI-powered coding sessions from your phone — no proprietary backend, no mandatory
accounts, no tracking by default.
'''Key features'''
* Multiple connection types — local network, secure tunnels (Cloudflare, ngrok),
or any self-hosted opencode instance
* Biometric unlock — fingerprint / PIN to keep your sessions private
* Real-time streaming chat — watch your AI agent think and respond live
* File diff viewer — see exactly what code changes the agent proposes
* Multi-session management — start, resume, and switch between coding sessions
* Tool call approval — review and approve agent actions before they run
'''Self-hosted first'''
The app requires your own opencode server. Your AI traffic goes directly to your
backend (Claude, GPT, Gemini, local models — your choice). No middleman required.
An optional hosted backend ("opencode Cloud") is planned as a future paid service,
but the client is always free and fully functional without it.
'''Crash reporting'''
Sentry crash reporting is opt-in with default OFF. Code, prompts, and AI responses
never leave your own server.
'''Anti-features'''
NonFreeNet: the opencode server you connect to may connect to proprietary AI
services (OpenAI, Anthropic, Google). The app itself contains no proprietary
network code.
AntiFeatures:
NonFreeNet:
en-US: >-
The app connects to a user-self-hosted opencode server which may in turn
connect to proprietary AI services (OpenAI API, Anthropic API, Google Gemini).
The app itself is fully open source and does not require any specific provider.
RepoType: git
Repo: https://github.com/dzianisv/opencode-mobile
Builds:
- versionName: '1.0.0'
versionCode: 1
commit: <FIRST_GITHUB_RELEASE_TAG>
subdir: android
sudo:
- apt-get update
- apt-get install -y nodejs npm
init:
- npm install --prefix .. --legacy-peer-deps
- npx --prefix .. expo prebuild --platform android --non-interactive
gradle:
- release
ndk: 26.1.10909125
# Node 20+ required for Expo SDK 52+
# prebuild regenerates android/ from app.json + package.json
# The signed AAB/APK is then compared against AllowedAPKSigningKeys
prebuild:
- cd .. && npm install --legacy-peer-deps
- cd .. && npx expo prebuild --platform android --non-interactive
# AllowedAPKSigningKeys pins our release signing key.
# F-Droid will serve our pre-signed APK rather than re-signing with their key.
# This requires reproducible builds (identical output across machines).
AllowedAPKSigningKeys: <SIGNING_KEY_SHA256_FINGERPRINT_LOWERCASE_NO_COLONS>
AutoUpdateMode: Version v%v
UpdateCheckMode: Tags
UpdateCheckData: https://raw.githubusercontent.com/dzianisv/opencode-mobile/main/app.json|"version":\s*"([^"]+)"|.|.
CurrentVersion: '1.0.0'
CurrentVersionCode: 1