From 791588647bb0551bc81ef4c0a7a1d2fc4b1cd314 Mon Sep 17 00:00:00 2001 From: Dennis V <2119348+dzianisv@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:16:36 +0000 Subject: [PATCH 1/5] feat(ios): add native build and TestFlight automation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ios-ci.yml | 137 ++++++++++ .github/workflows/publish-app-store.yml | 332 +++++++++++++----------- app.json | 1 + assets/icon.png | Bin 57923 -> 28677 bytes distribution/app-store-listing.md | 38 ++- distribution/ios-enrollment-runbook.md | 20 +- docs/applestore.md | 45 ++-- eas.json | 8 +- eas.json.README.md | 13 +- 9 files changed, 378 insertions(+), 216 deletions(-) create mode 100644 .github/workflows/ios-ci.yml diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml new file mode 100644 index 0000000..152fff4 --- /dev/null +++ b/.github/workflows/ios-ci.yml @@ -0,0 +1,137 @@ +# iOS build gate for pull requests and main. +# +# Requires NO Apple/EAS secrets: it validates the Expo config, exports the iOS JS +# bundle, generates the native project, installs CocoaPods, and compiles an +# UNSIGNED iPhone Simulator target with xcodebuild. Signing/TestFlight lives in +# publish-app-store.yml. +# +# Cheap platform-neutral checks (typecheck + unit tests) run first on Linux and +# gate the costly macOS native build. + +name: iOS CI + +on: + pull_request: + branches: [main] + paths-ignore: + - "**/*.md" + - "docs/**" + - "docs-site/**" + - "distribution/**" + push: + branches: [main] + paths-ignore: + - "**/*.md" + - "docs/**" + - "docs-site/**" + - "distribution/**" + +# Cancel superseded runs for the same ref (e.g. new push to an open PR). +concurrency: + group: ios-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Typecheck and unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + # Node >= 23.6 runs the TypeScript test files natively (type-stripping), + # matching the local toolchain. No build step or extra deps required. + node-version: 24 + cache: npm + + - name: Install dependencies (deterministic) + run: npm ci --legacy-peer-deps + + - name: Typecheck + run: npm run typecheck + + - name: Unit tests + run: npm test + + ios-build: + name: Unsigned iOS Simulator build + needs: test + # macos-15 ships Xcode 16.x, which React Native 0.81 / Expo SDK 54 require. + runs-on: macos-15 + timeout-minutes: 45 + env: + # No source-map upload from CI (no Sentry auth token here); keep the build hermetic. + SENTRY_DISABLE_AUTO_UPLOAD: "true" + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies (deterministic) + run: npm ci --legacy-peer-deps + + - name: Validate Expo config and plugin resolution + run: npx expo config --type introspect --json > expo-config.introspect.json + + - name: Export iOS JavaScript bundle + run: npx expo export --platform ios --output-dir dist + + - name: Prebuild native iOS project + run: npx expo prebuild --platform ios --no-install + + - name: Install CocoaPods dependencies + working-directory: ios + run: pod install + + - name: Resolve Xcode workspace and scheme + id: xc + run: | + set -euo pipefail + shopt -s nullglob + workspaces=(ios/*.xcworkspace) + workspace="${workspaces[0]:-}" + if [ -z "$workspace" ]; then + echo "::error::No .xcworkspace was generated by expo prebuild." + exit 1 + fi + scheme=$(xcodebuild -workspace "$workspace" -list -json | node -e " + const d = JSON.parse(require('fs').readFileSync(0, 'utf8')); + const all = (d.workspace && d.workspace.schemes) || []; + const app = all.filter((s) => s !== 'Pods' && !s.startsWith('Pods-')); + if (app.length === 0) { console.error('No application scheme found in workspace'); process.exit(1); } + process.stdout.write(app[0]); + ") + echo "workspace=$workspace" >> "$GITHUB_OUTPUT" + echo "scheme=$scheme" >> "$GITHUB_OUTPUT" + echo "Using workspace='$workspace' scheme='$scheme'" + + - name: Build unsigned iPhone Simulator app + run: | + set -euo pipefail + NSUnbufferedIO=YES xcodebuild \ + -workspace "${{ steps.xc.outputs.workspace }}" \ + -scheme "${{ steps.xc.outputs.scheme }}" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath ios/build \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + build 2>&1 | tee xcodebuild.log + + - name: Upload xcodebuild log + if: always() + uses: actions/upload-artifact@v7 + with: + name: ios-xcodebuild-log + path: xcodebuild.log + retention-days: 14 + if-no-files-found: ignore diff --git a/.github/workflows/publish-app-store.yml b/.github/workflows/publish-app-store.yml index 2426770..97e7bdf 100644 --- a/.github/workflows/publish-app-store.yml +++ b/.github/workflows/publish-app-store.yml @@ -1,45 +1,59 @@ -# STATUS: Validated structure — awaiting Apple Developer Program enrollment approval. -# Once enrollment is approved, complete these steps and this workflow is production-ready: +# Publish OpenCode for iOS to TestFlight with EAS Build + EAS Submit. # -# REMAINING GAPS (must complete before first run): -# 1. Update eas.json: replace REPLACE_WITH_APP_STORE_CONNECT_APP_ID and REPLACE_WITH_APPLE_TEAM_ID -# (see eas.json.README.md for exact click paths in App Store Connect) -# 2. Add GitHub secrets (Settings > Secrets and variables > Actions): -# EAS_TOKEN Expo access token (expo.dev > Account > Access Tokens) -# APPLE_APP_STORE_CONNECT_API_KEY_ID Key ID from App Store Connect > Users & Access > Integrations > App Store Connect API -# APPLE_APP_STORE_CONNECT_ISSUER_ID Issuer ID from same page -# APPLE_APP_STORE_CONNECT_API_KEY base64-encoded .p8 file (download at key creation — one time only) -# 3. Run `eas login` locally and `eas build:configure` on first run to let EAS set up signing -# 4. Manually upload first build to App Store Connect (required once to create the app record) +# This workflow FAILS FAST (non-zero exit) instead of "succeeding by skipping": +# a release with missing credentials or unfilled identifiers is a hard error, so a +# green run always means a real build was produced and submitted. # -# OPTIONAL secrets (crash reporting): -# EXPO_PUBLIC_SENTRY_DSN SENTRY_AUTH_TOKEN SENTRY_ORG SENTRY_PROJECT +# ── HUMAN GATE (one-time, after Apple Developer Program enrollment) ────────────── +# Complete ALL of the following before releasing. Do NOT invent any of these IDs. # -# Build strategy: EAS Build (Expo Application Services) -# - No Mac runner needed; Expo hosts macOS workers with managed certificates. -# - Cost: free tier (30 builds/month); upgrade to $19/month for unlimited/priority queue. -# - See distribution/ios-enrollment-runbook.md for full enrollment steps. -# - See eas.json.README.md for placeholder fill-in instructions. -# - Alternative (self-hosted Mac runner): see commented section at bottom of this file. +# 1. Fill and commit the eas.json placeholders (see eas.json.README.md for click paths): +# submit.production.ios.ascAppId REPLACE_WITH_APP_STORE_CONNECT_APP_ID → numeric App Store Connect App ID +# submit.production.ios.appleTeamId REPLACE_WITH_APPLE_TEAM_ID → 10-char Apple Team ID +# +# 2. Add GitHub Actions secrets (Settings → Secrets and variables → Actions): +# EXPO_TOKEN Expo access token (expo.dev → Account settings → Access tokens) +# APPLE_APP_STORE_CONNECT_API_KEY_ID ASC API Key ID (App Store Connect → Users and Access → Integrations → App Store Connect API) +# APPLE_APP_STORE_CONNECT_ISSUER_ID ASC API Issuer ID (same page) +# APPLE_APP_STORE_CONNECT_API_KEY base64 of the .p8 key file: `base64 -i AuthKey_XXXX.p8` (downloadable once) +# +# 3. Bootstrap iOS signing credentials on EAS once (creates the distribution cert + +# provisioning profile so CI never needs to prompt): +# eas login && eas build --platform ios --profile production +# +# Optional crash reporting belongs in the EAS `production` environment because the +# iOS bundle is built on a remote EAS worker. Configure EXPO_PUBLIC_SENTRY_DSN, +# SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT in Expo before releasing. +# +# Build number is managed remotely by EAS (eas.json: cli.appVersionSource=remote, +# build.production.ios.autoIncrement=buildNumber). No app.json mutation happens here. name: Publish to App Store (TestFlight) on: + # One release ⇒ one build. Triggering only on `release: published` avoids the + # duplicate build that a combined release+tag trigger would create. release: types: [published] - push: - tags: ["v*"] workflow_dispatch: -jobs: - publish-ios: - runs-on: ubuntu-latest - env: - EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ secrets.SENTRY_ORG }} - SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} +# Serialize runs per release so a re-trigger cannot start a duplicate concurrent +# build/submit. cancel-in-progress:false never kills an in-flight submission. +concurrency: + group: publish-app-store-${{ github.event.release.tag_name || github.ref_name }} + cancel-in-progress: false +permissions: + contents: read + +jobs: + testflight: + name: EAS Build and submit to TestFlight + runs-on: ubuntu-latest + timeout-minutes: 90 + env: + EAS_CLI_VERSION: "21.0.0" + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} steps: - uses: actions/checkout@v6 @@ -48,144 +62,148 @@ jobs: node-version: 20 cache: npm - - name: Check Apple prerequisites - id: check-apple - run: | - if [[ -n "${{ secrets.EAS_TOKEN }}" ]]; then - echo "proceed=true" >> "$GITHUB_OUTPUT" - else - echo "proceed=false" >> "$GITHUB_OUTPUT" - echo "::warning::Apple Developer enrollment pending — EAS_TOKEN not set. Skipping iOS build." - fi - - # Install EAS CLI globally. Pin to a recent stable version. - - name: Install EAS CLI - if: steps.check-apple.outputs.proceed == 'true' - run: npm install -g eas-cli@13 - - - name: Install dependencies - if: steps.check-apple.outputs.proceed == 'true' - run: npm install --legacy-peer-deps - - # Bump ios.buildNumber to match github.run_number (monotonically increasing). - # App Store Connect rejects duplicate build numbers for the same version string. - - name: Bump ios.buildNumber in app.json - if: steps.check-apple.outputs.proceed == 'true' - run: | - node -e " - const f = 'app.json'; - const j = require('./' + f); - j.expo.ios = j.expo.ios || {}; - j.expo.ios.buildNumber = String(${{ github.run_number }}); - require('fs').writeFileSync(f, JSON.stringify(j, null, 2) + '\n'); - " - echo "buildNumber now: $(node -p "require('./app.json').expo.ios.buildNumber")" - - # EAS Build: builds the IPA in Expo's cloud (macOS workers managed by Expo). - # --non-interactive: no prompts, suitable for CI. - # --platform ios: iOS only (Android is handled by publish-play-store.yml). - # --profile production: uses the "production" profile in eas.json (created below if missing). - - name: Build IPA via EAS - if: steps.check-apple.outputs.proceed == 'true' + - name: Preflight — verify credentials and identifiers (fail fast) env: - EXPO_TOKEN: ${{ secrets.EAS_TOKEN }} - APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} - APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }} - APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }} + ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }} + ASC_KEY_B64: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }} run: | + set -euo pipefail + fail=0 + need() { + if [ -z "${2:-}" ]; then + echo "::error::Missing required secret: $1" + fail=1 + fi + } + need "EXPO_TOKEN" "${EXPO_TOKEN:-}" + need "APPLE_APP_STORE_CONNECT_API_KEY_ID" "${ASC_KEY_ID:-}" + need "APPLE_APP_STORE_CONNECT_ISSUER_ID" "${ASC_ISSUER_ID:-}" + need "APPLE_APP_STORE_CONNECT_API_KEY" "${ASC_KEY_B64:-}" + asc_app_id=$(node -p "require('./eas.json').submit.production.ios.ascAppId || ''") + team_id=$(node -p "require('./eas.json').submit.production.ios.appleTeamId || ''") + case "$asc_app_id" in + ""|REPLACE_*) echo "::error::eas.json submit.production.ios.ascAppId is unset or still a placeholder"; fail=1 ;; + *[!0-9]*) echo "::error::eas.json submit.production.ios.ascAppId must contain only digits"; fail=1 ;; + esac + case "$team_id" in + ""|REPLACE_*) echo "::error::eas.json submit.production.ios.appleTeamId is unset or still a placeholder"; fail=1 ;; + esac + if ! printf '%s' "$team_id" | grep -Eq '^[A-Z0-9]{10}$'; then + echo "::error::eas.json submit.production.ios.appleTeamId must be a 10-character Apple Team ID" + fail=1 + fi + if [ -n "${ASC_KEY_ID:-}" ] && ! printf '%s' "$ASC_KEY_ID" | grep -Eq '^[A-Z0-9]{10}$'; then + echo "::error::APPLE_APP_STORE_CONNECT_API_KEY_ID must be a 10-character key ID" + fail=1 + fi + if [ -n "${ASC_ISSUER_ID:-}" ] && ! printf '%s' "$ASC_ISSUER_ID" | grep -Eq '^[0-9a-fA-F-]{36}$'; then + echo "::error::APPLE_APP_STORE_CONNECT_ISSUER_ID must be a UUID" + fail=1 + fi + if [ "$fail" -ne 0 ]; then + echo "::error::BLOCKED: complete the one-time human-gated setup in this workflow's header (GitHub secrets + eas.json identifiers) before releasing. No build was started." + exit 1 + fi + echo "Preflight OK — all credentials and identifiers present." + + - name: Install EAS CLI (exact pin) + run: npm install -g eas-cli@"$EAS_CLI_VERSION" + + - name: Install dependencies (deterministic) + run: npm ci --legacy-peer-deps + + - name: Configure App Store Connect API key + env: + ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }} + ASC_KEY_B64: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }} + run: | + set -euo pipefail + key_path="$RUNNER_TEMP/asc_api_key.p8" + printf '%s' "$ASC_KEY_B64" | base64 -d > "$key_path" + if ! head -n1 "$key_path" | grep -q "BEGIN PRIVATE KEY"; then + echo "::error::APPLE_APP_STORE_CONNECT_API_KEY did not base64-decode to a valid .p8 private key." + exit 1 + fi + chmod 600 "$key_path" + export ASC_KEY_PATH="$key_path" + # Expose ASC credentials to EAS Build for non-interactive signing management. + { + echo "EXPO_ASC_API_KEY_PATH=$key_path" + echo "EXPO_ASC_KEY_ID=$ASC_KEY_ID" + echo "EXPO_ASC_ISSUER_ID=$ASC_ISSUER_ID" + echo "EXPO_APPLE_TEAM_ID=$(node -p "require('./eas.json').submit.production.ios.appleTeamId")" + echo "EXPO_APPLE_TEAM_TYPE=COMPANY_OR_ORGANIZATION" + } >> "$GITHUB_ENV" + # EAS Submit reads the ASC key only from the eas.json submit profile (all three + # fields required). Inject them here so no real key IDs are committed to the repo. + node -e " + const fs = require('fs'); + const j = require('./eas.json'); + j.submit.production.ios.ascApiKeyPath = process.env.ASC_KEY_PATH; + j.submit.production.ios.ascApiKeyId = process.env.ASC_KEY_ID; + j.submit.production.ios.ascApiKeyIssuerId = process.env.ASC_ISSUER_ID; + fs.writeFileSync('eas.json', JSON.stringify(j, null, 2) + '\n'); + " + echo "ASC API key configured for EAS Build and EAS Submit." + + - name: EAS Build (iOS, wait for completion) + id: build + run: | + set -uo pipefail + set +e eas build \ --platform ios \ --profile production \ --non-interactive \ - --no-wait \ - --json \ - | tee eas-build-output.json - BUILD_ID=$(cat eas-build-output.json | node -e "const d=require('fs').readFileSync('/dev/stdin','utf8');console.log(JSON.parse(d).id)") - echo "EAS_BUILD_ID=$BUILD_ID" >> $GITHUB_ENV - echo "Build ID: $BUILD_ID" + --json > eas-build-output.json + rc=$? + set -e + if [ "$rc" -ne 0 ]; then + echo "::error::eas build failed (exit $rc). See the eas-ios-build-metadata artifact." + exit "$rc" + fi + # `eas build --json` prints a JSON ARRAY of completed builds. Select the exact + # iOS build id deterministically — never rely on an ambiguous "latest". + build_id=$(node -e " + const a = JSON.parse(require('fs').readFileSync('eas-build-output.json', 'utf8')); + if (!Array.isArray(a)) { console.error('Expected a JSON array from eas build --json'); process.exit(1); } + const ios = a.filter((b) => String(b.platform).toUpperCase() === 'IOS'); + if (ios.length !== 1) { console.error('Expected exactly one iOS build, got ' + ios.length); process.exit(1); } + const b = ios[0]; + if (b.status && String(b.status).toUpperCase() !== 'FINISHED') { console.error('iOS build did not finish: ' + b.status); process.exit(1); } + if (!b.id) { console.error('Build object has no id'); process.exit(1); } + process.stdout.write(b.id); + ") + echo "build_id=$build_id" >> "$GITHUB_OUTPUT" + echo "Selected EAS iOS build id: $build_id" - # Wait for the EAS build to complete (iOS builds typically take 15–25 minutes). - - name: Wait for EAS build - if: steps.check-apple.outputs.proceed == 'true' - env: - EXPO_TOKEN: ${{ secrets.EAS_TOKEN }} - run: | - echo "Waiting for build $EAS_BUILD_ID to complete..." - eas build:view "$EAS_BUILD_ID" --json --wait - echo "Build complete." + - name: Upload EAS build metadata + if: always() + uses: actions/upload-artifact@v7 + with: + name: eas-ios-build-metadata + path: eas-build-output.json + retention-days: 30 + if-no-files-found: ignore - # Submit to TestFlight via EAS Submit. Uses the same App Store Connect API key. - # --latest: picks the most recent finished build for this app + platform. - - name: Submit to TestFlight via EAS Submit - if: steps.check-apple.outputs.proceed == 'true' - env: - EXPO_TOKEN: ${{ secrets.EAS_TOKEN }} - APPLE_APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} - APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }} - APPLE_APP_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }} + - name: Submit exact build to TestFlight run: | + set -euo pipefail eas submit \ --platform ios \ - --id "$EAS_BUILD_ID" \ + --profile production \ + --id "${{ steps.build.outputs.build_id }}" \ --non-interactive - # Upload release notes to TestFlight (what's new text for testers). - # NOTE: EAS Submit does not yet support whatsNew natively; use fastlane pilot - # or App Store Connect API directly if per-build release notes are needed. - - name: Upload TestFlight release notes (informational) - if: steps.check-apple.outputs.proceed == 'true' + - name: TestFlight release notes (informational) + if: always() run: | - echo "TestFlight release notes for this build:" - cat distribution/whatsnew-ios/release-notes-en-US.txt - -# --------------------------------------------------------------------------- -# ALTERNATIVE: Self-hosted Mac runner (macbook13-pro at 100.68.120.26) -# --------------------------------------------------------------------------- -# To use the Mac mini instead of EAS Build: -# 1. SSH to macbook13-pro and set up GitHub self-hosted runner: -# https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners -# 2. Change "runs-on: ubuntu-latest" above to "runs-on: self-hosted" -# and add label "macos" for clarity. -# 3. Replace the EAS Build + Submit steps with: -# -# - name: Install CocoaPods -# run: sudo gem install cocoapods -# -# - name: Expo prebuild (iOS) -# run: npx expo prebuild --platform ios --no-install -# -# - name: Install CocoaPods dependencies -# working-directory: ios -# run: pod install -# -# - name: Build IPA -# run: | -# xcodebuild -workspace ios/opencodemobile.xcworkspace \ -# -scheme opencodemobile \ -# -sdk iphoneos \ -# -configuration Release \ -# -archivePath $RUNNER_TEMP/opencodemobile.xcarchive \ -# archive \ -# CODE_SIGN_STYLE=Manual \ -# DEVELOPMENT_TEAM=${{ secrets.APPLE_TEAM_ID }} \ -# CODE_SIGN_IDENTITY="Apple Distribution" \ -# PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}" -# -# - name: Export IPA -# run: | -# xcodebuild -exportArchive \ -# -archivePath $RUNNER_TEMP/opencodemobile.xcarchive \ -# -exportOptionsPlist ios/ExportOptions.plist \ -# -exportPath $RUNNER_TEMP/export -# -# - name: Upload to TestFlight (xcrun altool / notarytool) -# run: | -# xcrun altool --upload-app \ -# -f "$RUNNER_TEMP/export/opencodemobile.ipa" \ -# --type ios \ -# --apiKey "${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}" \ -# --apiIssuer "${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}" -# -# Self-hosted runner cost: $0 compute (your hardware), but requires maintaining -# a macOS machine with Xcode, certificates, and provisioning profiles. -# EAS Build is strongly recommended for the first release. + notes="distribution/whatsnew-ios/release-notes-en-US.txt" + if [ -f "$notes" ]; then + echo "TestFlight 'What to Test' notes for this release:" + cat "$notes" + else + echo "No release notes file found at $notes" + fi diff --git a/app.json b/app.json index 5bfcaf1..f66fb07 100644 --- a/app.json +++ b/app.json @@ -25,6 +25,7 @@ "NSMicrophoneUsageDescription": "OpenCode uses the microphone to capture your voice when using speech-to-text input.", "NSPhotoLibraryUsageDescription": "OpenCode can attach images from your photo library to messages to your AI coding agent.", "NSCameraUsageDescription": "OpenCode can capture images with your camera and attach them to messages to your AI coding agent.", + "NSLocalNetworkUsageDescription": "OpenCode uses your local network to connect to self-hosted OpenCode servers running on your LAN.", "NSAppTransportSecurity": { "NSAllowsArbitraryLoads": true, "NSAllowsArbitraryLoadsInWebContent": false diff --git a/assets/icon.png b/assets/icon.png index 9e16101d2351169792ba7f55ee3e28c3e72197fd..e13b95dfd122ac744a009327b53900aec41392e3 100644 GIT binary patch literal 28677 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 13.0.0" + "version": ">= 21.0.0", + "appVersionSource": "remote" }, "build": { "development": { @@ -17,9 +18,10 @@ } }, "production": { - "autoIncrement": false, + "environment": "production", "ios": { - "distribution": "store" + "distribution": "store", + "autoIncrement": "buildNumber" }, "android": { "buildType": "app-bundle" diff --git a/eas.json.README.md b/eas.json.README.md index 76f5353..5dde065 100644 --- a/eas.json.README.md +++ b/eas.json.README.md @@ -62,20 +62,21 @@ Alternatively, in App Store Connect: | Field | Value | Notes | |---|---|---| -| `appleId` | `appstore@agentlabs.cc` | The Apple ID used for App Store Connect login — update if different | +| `appleId` | `support@agentlabs.cc` | The Apple ID used for App Store Connect login — update if different | | `distribution` (production ios) | `store` | Correct for App Store / TestFlight submissions | | `buildType` (production android) | `app-bundle` | Correct for Play Store AAB submissions | -| `autoIncrement` | `false` | Build number is bumped by the CI workflow (github.run_number), not EAS | -| `cli.version` | `>= 13.0.0` | Requires EAS CLI 13 or later; CI installs `eas-cli@13` | +| `autoIncrement` | `buildNumber` | EAS increments the iOS build number remotely for every production build | +| `appVersionSource` | `remote` | EAS is the source of truth for store build numbers | +| `cli.version` | `>= 21.0.0` | CI installs the exact supported release, `eas-cli@21.0.0` | --- ## After filling in the placeholders 1. Commit the updated `eas.json` to the repo. -2. Add the GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` header for the exact list). -3. Tag a release: `git tag v0.2.3 && git push --tags` -4. The CI workflow will build the IPA via EAS and submit it to TestFlight automatically. +2. Add the `EXPO_TOKEN` and App Store Connect API GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` for the exact list). +3. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow). +4. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight. --- From 679475d0a4f8e6592b77e31ecce9ca9a97d7cfce Mon Sep 17 00:00:00 2001 From: Dennis V <2119348+dzianisv@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:16:36 +0000 Subject: [PATCH 2/5] docs(site): publish iOS implementation progress Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs-site/download/index.html | 11 ++++++ docs-site/index.html | 4 +- docs-site/ios/index.html | 54 +++++++++++++------------- docs-site/opencode-on-phone/index.html | 2 +- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/docs-site/download/index.html b/docs-site/download/index.html index a14cf57..64ed753 100644 --- a/docs-site/download/index.html +++ b/docs-site/download/index.html @@ -224,6 +224,17 @@ footer a{margin:0 8px}

+
+ diff --git a/docs-site/index.html b/docs-site/index.html index 972850c..2132c5b 100644 --- a/docs-site/index.html +++ b/docs-site/index.html @@ -87,7 +87,7 @@ "name": "Is there an iOS app?", "acceptedAnswer": { "@type": "Answer", - "text": "No. OpenCode Mobile is Android only. There is no iOS or iPadOS build." + "text": "Not for download yet. The iPhone and iPad build is in active development, including native build CI and TestFlight automation." } }, { @@ -392,7 +392,7 @@ OPENCODE_SERVER_PASSWORD=yourpassword opencode serve --hostname 0.0.0.0 --port 4
Is there an iOS app? -

No. OpenCode Mobile is Android only. There is no iOS or iPadOS build. More on the iOS / iPhone page.

+

Not for download yet. The iPhone and iPad build is in active development, including native build CI and TestFlight automation. Follow the iOS / iPhone progress page.

Where do my API keys live? diff --git a/docs-site/ios/index.html b/docs-site/ios/index.html index 03a0c0b..39f5c5f 100644 --- a/docs-site/ios/index.html +++ b/docs-site/ios/index.html @@ -3,8 +3,8 @@ -OpenCode for iOS / iPhone — Is There an App? (Honest Answer) - +OpenCode for iOS / iPhone — TestFlight Work in Progress + @@ -13,15 +13,15 @@ - - + + - - + + @@ -45,22 +45,22 @@ { "@type": "Question", "name": "Is there an OpenCode app for iOS or iPhone?", - "acceptedAnswer": { "@type": "Answer", "text": "Not yet. OpenCode Mobile is currently Android only. There is no iOS, iPhone, or iPadOS build, and none is published on the App Store. The only official mobile app today is the Android client." } + "acceptedAnswer": { "@type": "Answer", "text": "Not for download yet. The shared React Native app is configured for iPhone and iPad, and native build plus TestFlight automation is in active development. No iOS build is published on TestFlight or the App Store today." } }, { "@type": "Question", - "name": "Why is there no OpenCode iOS app?", - "acceptedAnswer": { "@type": "Answer", "text": "It is a small, volunteer-driven open-source project. Building started on Android, and an iOS release also needs a paid Apple Developer account plus App Store review. The team would rather ship one platform well than two poorly. An iOS port is possible but not yet committed." } + "name": "What remains before the OpenCode iOS beta?", + "acceptedAnswer": { "@type": "Answer", "text": "The generated native project and CI must pass on macOS, then Apple Developer and App Store Connect credentials must be configured so the first build can be submitted to TestFlight." } }, { "@type": "Question", "name": "Can I use OpenCode on my iPhone right now in any way?", - "acceptedAnswer": { "@type": "Answer", "text": "There is no native iOS client. Because opencode runs as a server you control, you can reach its web interface from a mobile browser on iOS, but it is not optimized for phones. For a proper mobile experience today, the Android app is the supported option." } + "acceptedAnswer": { "@type": "Answer", "text": "There is no downloadable native iOS build yet. Because opencode runs as a server you control, you can reach its web interface from a mobile browser on iOS, but it is not optimized for phones. Android remains the supported native option until TestFlight opens." } }, { "@type": "Question", "name": "How will I know when an iOS app is available?", - "acceptedAnswer": { "@type": "Answer", "text": "Watch or star the project on GitHub. Releases and any iOS news are announced there. You can also open or upvote a feature request to signal demand." } + "acceptedAnswer": { "@type": "Answer", "text": "Follow GitHub issue 65 for the implementation checklist, CI status, and TestFlight milestone. Releases will be announced from the repository." } } ] } @@ -168,15 +168,15 @@ footer a{margin:0 8px}

OpenCode for iOS / iPhone

-

The honest answer: there isn't an iOS app yet. OpenCode Mobile is currently Android only — there is no iPhone or iPad build, and nothing on the App Store. If you landed here hoping for an iOS download, we'd rather tell you straight than waste your time.

-

Here's the full picture: what exists today, why iOS isn't available yet, what your options are right now, and how to be the first to know if that changes.

+

The iOS build is now in active development. OpenCode Mobile is configured for iPhone and iPad, with native build and TestFlight CI work underway. It is not downloadable yet and nothing is published on the App Store.

+

This page tracks what is already complete, what still needs macOS and Apple access, and where to follow the launch.

What exists today

-

OpenCode Mobile is a free, open-source (MIT) Android client for the opencode AI coding agent. It's live now — v0.4.3 — installable via F-Droid or a direct APK. It connects your phone to an opencode server you run yourself, so your code and AI provider keys stay on your own machine, with no telemetry by default.

+

OpenCode Mobile is a free, open-source (MIT) client for the opencode AI coding agent. Android is installable today through F-Droid or a direct APK. The same React Native product is now configured for iOS with bundle ID cc.agentlabs.opencode, an App Store icon and metadata, local-network permission, and automated build work.

If you have an Android phone (or a spare one), it works today. See the setup guide or learn how to use opencode on your phone.

@@ -185,13 +185,13 @@ footer a{margin:0 8px}
-

Why there's no iOS app (yet)

+

What remains before TestFlight

    -
  • It's a small, volunteer open-source project. Development started on Android, and maintaining a quality second platform is real, ongoing work.
  • -
  • iOS has extra overhead. Shipping to iPhone needs a paid Apple Developer account and App Store review — a higher bar than Android's open distribution (F-Droid, direct APK).
  • -
  • Focus beats half-finished. The goal is to do one platform well rather than ship two mediocre apps. An iOS port is technically feasible but not yet committed.
  • +
  • Native macOS verification. The generated Xcode project must build and launch in iPhone and iPad Simulators, then the critical connection and chat flow must be exercised.
  • +
  • Apple configuration. The Apple Developer membership, App Store Connect app record, team ID, and API credentials must be active.
  • +
  • TestFlight proof. Release CI must build and submit the exact signed archive, and the build must appear for internal testers before we call iOS available.
-

No vaporware promises here. If an iOS build happens, it'll be announced on GitHub — not pre-sold on a landing page.

+

The public checklist and implementation live in GitHub issue #65. Availability will be claimed only after a real TestFlight install is verified.

@@ -201,19 +201,19 @@ footer a{margin:0 8px}
  • Use an Android device. Even an inexpensive or spare Android phone runs the full app today.
  • Use the opencode web/desktop interface. Since opencode runs as a server you control, you can reach it from a desktop or — unoptimized — from a mobile browser. It isn't a phone-native experience, but it works.
  • -
  • Register your interest. Open or upvote a feature request on GitHub so the demand for iOS is visible.
  • +
  • Follow the build. Track issue #65 for native build, CI, and TestFlight progress.
-

Roadmap & how to follow

-

There's no committed iOS release date. The most reliable way to hear about it — and every other release — is straight from the repo:

+

Launch progress & how to follow

+

Implementation is committed; a release date is not. The remaining Mac and Apple-gated work is tracked in public:

Watching the repo notifies you of new releases, including any future iOS news.

@@ -224,15 +224,15 @@ footer a{margin:0 8px}

Frequently asked questions

Is there an OpenCode app for iOS or iPhone? -

Not yet. OpenCode Mobile is Android only. There is no iOS, iPhone, or iPadOS build, and none on the App Store.

+

Not for download yet. The iPhone and iPad build is in active development, but there is no verified TestFlight or App Store release today.

Can I use OpenCode on my iPhone at all right now? -

There's no native iOS client. Because opencode runs as a server you control, you can open its web interface from a mobile browser, but it isn't optimized for phones. For a proper mobile experience, the Android app is the supported option.

+

There's no downloadable native iOS build yet. Because opencode runs as a server you control, you can open its web interface from a mobile browser, but it isn't optimized for phones. Android remains the supported native option until TestFlight opens.

- Will an iOS app ever come? -

Possibly. It's technically feasible but not committed. Demand on GitHub helps; any news will be announced there.

+ Is the iOS app committed? +

Yes. Native build, CI, and TestFlight work is active in issue #65. The launch still depends on macOS verification and Apple credentials.

Is the Android app free and private? diff --git a/docs-site/opencode-on-phone/index.html b/docs-site/opencode-on-phone/index.html index 4e8eb8e..c4c3e7a 100644 --- a/docs-site/opencode-on-phone/index.html +++ b/docs-site/opencode-on-phone/index.html @@ -325,7 +325,7 @@ footer a{margin:0 8px}
Does it work on iPhone? -

Not yet. OpenCode Mobile is Android-only today. iOS is on the roadmap.

+

Not for download yet. The iPhone and iPad build is in active development; follow the iOS progress page for native build and TestFlight status.

From f9b18a06f31873892d07294be07e68afd1c0168f Mon Sep 17 00:00:00 2001 From: Dennis V <2119348+dzianisv@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:30:09 +0000 Subject: [PATCH 3/5] fix(privacy): stop telemetry on consent revocation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/(tabs)/settings.tsx | 17 +++++- app/connection/[id].tsx | 2 +- app/connection/add.tsx | 2 +- distribution/app-store-listing.md | 11 ++-- distribution/privacy-policy.html | 2 +- distribution/privacy-policy.md | 2 +- src/lib/sentry.ts | 88 +++++++++++++++++++++++-------- src/lib/telemetry.ts | 36 ++++++++----- 8 files changed, 114 insertions(+), 46 deletions(-) diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index f2d4d69..f2e412e 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -73,14 +73,26 @@ export default function SettingsScreen() { const { settings, hasBiometrics, updateSettings, lock } = useAuth() const { notifications, setNotification } = useSettings() const [osGranted, setOsGranted] = useState(null) + const [telemetryUpdating, setTelemetryUpdating] = useState(false) // Telemetry consent: hasTelemetryConsent() returns null (unknown), true, or false. // We initialise local state from in-memory value; updates call setTelemetryConsent(). const [crashReporting, setCrashReporting] = useState(hasTelemetryConsent() ?? false) const handleCrashReportingToggle = useCallback(async (value: boolean) => { - setCrashReporting(value) - await setTelemetryConsent(value) + setTelemetryUpdating(true) + try { + await setTelemetryConsent(value) + setCrashReporting(value) + } catch { + setCrashReporting(hasTelemetryConsent() ?? false) + Alert.alert( + "Privacy Setting Not Saved", + "Crash reporting is off for this session, but your choice could not be saved. Please try again.", + ) + } finally { + setTelemetryUpdating(false) + } }, []) // Check OS permission state on first toggle attempt @@ -193,6 +205,7 @@ export default function SettingsScreen() { } diff --git a/app/connection/[id].tsx b/app/connection/[id].tsx index 73bd9b4..4328056 100644 --- a/app/connection/[id].tsx +++ b/app/connection/[id].tsx @@ -98,7 +98,7 @@ export default function EditConnectionScreen() { url.trim(), username.trim() && password ? { username: username.trim(), password } : undefined, ) - captureDiagnostic(report, result.error ? new Error(result.error) : undefined) + captureDiagnostic(report) setIsTesting(false) Alert.alert("Connection Failed", `${report.summary}\n\n(${result.error || "no detail"})`, [ diff --git a/app/connection/add.tsx b/app/connection/add.tsx index 40d2565..34416f3 100644 --- a/app/connection/add.tsx +++ b/app/connection/add.tsx @@ -100,7 +100,7 @@ export default function AddConnectionScreen() { serverUrl, username.trim() && password ? { username: username.trim(), password } : undefined, ) - captureDiagnostic(report, result.error ? new Error(result.error) : undefined) + captureDiagnostic(report) setIsConnecting(false) Alert.alert( "Connection Failed", diff --git a/distribution/app-store-listing.md b/distribution/app-store-listing.md index 6566c31..9e6f594 100644 --- a/distribution/app-store-listing.md +++ b/distribution/app-store-listing.md @@ -171,7 +171,7 @@ OpenCode Mobile does NOT collect any of the following: - Browsing history, search history - Sensitive info - User content (code, prompts, AI responses are not sent to our servers) -- Identifiers (User ID, Device ID — Sentry uses an installation-scoped anonymous ID, see below) +- User ID (the app has no accounts or user identity) ### Data Linked to You: None @@ -179,13 +179,14 @@ OpenCode Mobile does NOT collect any of the following: | Data Type | Category | Purpose | Optional? | |---|---|---|---| -| Crash Data | Diagnostics | App functionality | No — always on (see note) | -| Performance Data | Diagnostics | App functionality | No — always on (see note) | -| Other Diagnostic Data | Diagnostics | App functionality | No | +| Crash Data | Diagnostics | App functionality | Yes — explicit opt-in | +| Performance Data | Diagnostics | App functionality | Yes — explicit opt-in | +| Other Diagnostic Data | Diagnostics | App functionality | Yes — explicit opt-in | +| Device ID | Identifiers | App functionality | Yes — Sentry installation ID with explicit opt-in | **Explanation**: Sentry crash reporting sends device model, OS version, app version, and stack traces. Sentry assigns an anonymous installation ID (not linked to any Apple ID or personal information). No user-generated content (code, prompts, responses) is ever sent. -**Sentry opt-in status**: As of v0.2.3, Sentry is **always on** when a DSN is configured. If you add a settings toggle for Sentry consent (planned), change Optional? to "Yes" and add a note that users who decline are in the "Data Not Collected" category. In App Store Connect, once opt-in is implemented, this section can be removed or marked optional. +**Sentry opt-in status**: Crash reporting is off by default. The first-launch consent prompt and Settings → Privacy toggle control it. Declining does not initialize Sentry; turning it off later closes the active SDK and stops new event capture. **"Are you or your third-party partners using this data to track users?"**: No diff --git a/distribution/privacy-policy.html b/distribution/privacy-policy.html index b9921a6..19c370f 100644 --- a/distribution/privacy-policy.html +++ b/distribution/privacy-policy.html @@ -186,7 +186,7 @@
  • Open the app → Settings → Privacy → Crash reporting toggle.
  • -
  • When the toggle is off, Sentry is never initialised and no data leaves your device.
  • +
  • If you decline, Sentry is never initialised. If you turn reporting off later, the active SDK is closed and no new events are captured.

5. Third-Party Services

diff --git a/distribution/privacy-policy.md b/distribution/privacy-policy.md index 170d3a6..70776ac 100644 --- a/distribution/privacy-policy.md +++ b/distribution/privacy-policy.md @@ -55,7 +55,7 @@ URL scrubbing: before any event is sent to Sentry, our code strips all server UR Crash reporting is **opt-in and off by default**. On first launch you will see a consent prompt. You can change this at any time: - Open the app → **Settings** → **Privacy** → **Crash reporting** toggle. -- When the toggle is off, Sentry is never initialised and no data leaves your device. +- If you decline, Sentry is never initialised. If you turn reporting off later, the active SDK is closed and no new events are captured. --- diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index 015f3a1..2655c32 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -13,17 +13,18 @@ import * as Sentry from "@sentry/react-native" import appJson from "../../app.json" import { log } from "./logbuffer" import type { DiagnosticReport } from "./diagnostics" -import { scrubUrl, scrubString, scrubObject } from "./scrub" const DSN = process.env.EXPO_PUBLIC_SENTRY_DSN const APP_VERSION = (appJson as { expo?: { version?: string } }).expo?.version ?? "unknown" let enabled = false +let handlersInstalled = false export function initSentry() { + if (enabled) return if (!DSN) { log.info("sentry", "no DSN configured — telemetry disabled") - installGlobalHandlers(false) + installGlobalHandlers() return } try { @@ -50,9 +51,9 @@ export function initSentry() { }, beforeBreadcrumb(crumb) { if (crumb.data && typeof crumb.data === "object") { - crumb.data = scrubObject(crumb.data as Record) + crumb.data = redactObject(crumb.data as Record) } - if (typeof crumb.message === "string") crumb.message = scrubString(crumb.message) + if (typeof crumb.message === "string") crumb.message = redactString(crumb.message) return crumb }, }) @@ -62,7 +63,14 @@ export function initSentry() { } catch (e) { log.warn("sentry", "init failed", String(e)) } - installGlobalHandlers(enabled) + installGlobalHandlers() +} + +export async function disableSentry() { + if (!enabled) return + enabled = false + await Sentry.close() + log.info("sentry", "disabled by user") } // Install belt-and-braces global handlers. The Sentry RN SDK already wires @@ -72,7 +80,10 @@ export function initSentry() { // shared diagnostic report) even when Sentry is disabled. // * Telemetry-disabled builds still leave a breadcrumb that something blew // up, which is invaluable when triaging a user-shared report offline. -function installGlobalHandlers(sentryEnabled: boolean) { +function installGlobalHandlers() { + if (handlersInstalled) return + handlersInstalled = true + type GlobalErrorUtils = { getGlobalHandler?: () => (err: unknown, isFatal?: boolean) => void setGlobalHandler?: (handler: (err: unknown, isFatal?: boolean) => void) => void @@ -83,7 +94,7 @@ function installGlobalHandlers(sentryEnabled: boolean) { errorUtils.setGlobalHandler((err: unknown, isFatal?: boolean) => { const error = toError(err) log.error("crash", isFatal ? "FATAL" : "non-fatal", error.message, error.stack ?? "") - if (sentryEnabled) { + if (enabled) { Sentry.captureException(error, (scope) => { scope.setLevel(isFatal ? "fatal" : "error") scope.setTag("crash.source", "js-global") @@ -104,7 +115,7 @@ function installGlobalHandlers(sentryEnabled: boolean) { g.onunhandledrejection = (event) => { const error = toError(event?.reason) log.error("crash", "unhandled-rejection", error.message, error.stack ?? "") - if (sentryEnabled) { + if (enabled) { Sentry.captureException(error, (scope) => { scope.setLevel("error") scope.setTag("crash.source", "promise-rejection") @@ -130,24 +141,62 @@ function toError(value: unknown): Error { export { scrubUrl } from "./scrub" function scrubEvent(event: T): T { - if (event.request?.url) event.request.url = scrubUrl(event.request.url) - if (event.message) event.message = scrubString(event.message) + if (event.request?.url) event.request.url = "" + if (event.message) event.message = redactString(event.message) if (event.exception?.values) { for (const ex of event.exception.values) { - if (ex.value) ex.value = scrubString(ex.value) + if (ex.value) ex.value = redactString(ex.value) } } if (event.breadcrumbs) { for (const crumb of event.breadcrumbs) { - if (typeof crumb.message === "string") crumb.message = scrubString(crumb.message) + if (typeof crumb.message === "string") crumb.message = redactString(crumb.message) if (crumb.data && typeof crumb.data === "object") { - crumb.data = scrubObject(crumb.data as Record) + crumb.data = redactObject(crumb.data as Record) } } } return event } +function redactString(value: string): string { + return value.replace(/https?:\/\/[^\s)\]}"']+/gi, "") +} + +function redactObject(value: Record): Record { + const redacted: Record = {} + for (const [key, item] of Object.entries(value)) { + if ( + /^(?:id|.*Id|.*ID|url|host|hostname|port|address|server|serverUrl|target|endpoint|authorization|auth|token|password|secret|apiKey|username|cookie)$/i.test( + key, + ) + ) { + redacted[key] = "" + continue + } + if (typeof item === "string") { + redacted[key] = redactString(item) + continue + } + if (Array.isArray(item)) { + redacted[key] = item.map((entry) => + typeof entry === "string" + ? redactString(entry) + : entry && typeof entry === "object" + ? redactObject(entry as Record) + : entry, + ) + continue + } + if (item && typeof item === "object") { + redacted[key] = redactObject(item as Record) + continue + } + redacted[key] = item + } + return redacted +} + // --- Helpers exposed to the rest of the app ------------------------------ export type Breadcrumb = { @@ -183,18 +232,14 @@ export function captureException( }) } -export function captureDiagnostic(report: DiagnosticReport, rawError?: unknown) { +export function captureDiagnostic(report: DiagnosticReport) { log.info("sentry", "capture", report.classification, enabled ? "(uploading)" : "(local only)") if (!enabled) return Sentry.withScope((scope) => { scope.setTag("connect.classification", report.classification) scope.setTag("connect.scheme", report.scheme ?? "n/a") scope.setContext("connection", { - url: scrubUrl(report.url), - host: report.host, - port: report.port, - isHostname: report.isHostname, - summary: report.summary, + targetType: report.isHostname ? "hostname" : "ip-address", }) scope.setContext("probes", { attempts: report.attempts.map((a) => ({ @@ -202,13 +247,10 @@ export function captureDiagnostic(report: DiagnosticReport, rawError?: unknown) ok: a.ok, status: a.status, durationMs: a.durationMs, - error: a.error, - cause: a.errorCause, })), }) scope.setContext("device", report.device) - const err = rawError instanceof Error ? rawError : new Error(`connect ${report.classification}: ${report.summary}`) - Sentry.captureException(err) + Sentry.captureException(new Error(`connect ${report.classification}`)) }) } diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index 9092cc8..b259dd6 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -20,13 +20,14 @@ */ import * as SecureStore from "expo-secure-store" -import { initSentry, sentryEnabled } from "./sentry" +import { disableSentry, initSentry, sentryEnabled } from "./sentry" const CONSENT_KEY = "opencode_telemetry_consent" export type ConsentState = "granted" | "denied" | "unknown" let _resolved: boolean | null = null // null = unknown, true = granted, false = denied +let transition = Promise.resolve() /** * Load persisted consent from SecureStore. @@ -47,9 +48,8 @@ export async function loadTelemetryConsent(): Promise { _resolved = null return "unknown" } catch { - // SecureStore unavailable — don't clobber a previously resolved in-memory state. - // Return unknown so the caller can surface the modal. - return "unknown" + _resolved = false + return "denied" } } @@ -67,14 +67,26 @@ export function hasTelemetryConsent(): boolean | null { * Persist the user's consent decision and, if granted and Sentry is not yet * running, initialise it immediately. */ -export async function setTelemetryConsent(granted: boolean): Promise { - _resolved = granted - try { - await SecureStore.setItemAsync(CONSENT_KEY, granted ? "granted" : "denied") - } catch { - // Best-effort persist — in-memory state is still correct for this session. +export function setTelemetryConsent(granted: boolean): Promise { + const next = transition.then(() => applyTelemetryConsent(granted)) + transition = next.catch(() => undefined) + return next +} + +async function applyTelemetryConsent(granted: boolean): Promise { + if (granted) { + await SecureStore.setItemAsync(CONSENT_KEY, "granted") + _resolved = true + if (!sentryEnabled()) initSentry() + return } - if (granted && !sentryEnabled()) { - initSentry() + + _resolved = false + await disableSentry() + try { + await SecureStore.setItemAsync(CONSENT_KEY, "denied") + } catch (error) { + await SecureStore.deleteItemAsync(CONSENT_KEY) + throw error } } From c287ecad70a3f0a131372ac5929d5517674998d3 Mon Sep 17 00:00:00 2001 From: Dennis V <2119348+dzianisv@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:56:56 +0000 Subject: [PATCH 4/5] docs(agents): switch browser automation to VibeBrowser Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 758fc9b..de40c65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -220,28 +220,27 @@ These same secrets are set as GitHub Actions secrets on `dzianisv/opencode-mobil **Do NOT store secrets in `.env` files committed to the repo.** `.env` is gitignored — local copy only. -## Chrome DevTools (Browser Automation) +## VibeBrowser CLI (Browser Automation) -The project uses `@vibebrowser/chrome-devtools-mcp` from `github.com/dzianisv/chrome-devtools-mcp`. It runs an MCP server over HTTP/SSE, allowing multiple agents to connect remotely. +Use `@vibebrowser/cli` against the authenticated remote browser relay. Do not +register or use the retired `chrome-devtools` MCP server in Copilot. -**No `--remote-debugging-port` needed.** The daemon discovers Chrome via `--autoConnect` (reads `DevToolsActivePort` file). Port is discovered automatically. +Keep the relay URL outside the repository: -**Driving an authenticated session:** The daemon connects to your *real* Chrome profile (authenticated). To act on a logged-in page, use `list_pages` → `select_page` on the existing tab. Do **not** pass `isolatedContext` to `new_page` — that opens a cookieless context that cannot see your login. (Verified: the MCP code already uses `browser.defaultBrowserContext()`; logged-out sessions come from misuse, a profile/`--user-data-dir` mismatch, or Google's anti-automation block — not a tool bug.) - -**Local Copilot CLI config** (in `.github/copilot-mcp.json` or IDE MCP settings): -```json -{ - "chrome-devtools": { - "type": "remote", - "url": "http://localhost:9333/mcp", - "enabled": true - } -} +```bash +export VIBEBROWSER_REMOTE_URL='wss://relay.api.vibebrowser.app/' +npx -y @vibebrowser/cli@0.2.12 \ + --remote "$VIBEBROWSER_REMOTE_URL" --json status ``` -**Starting the daemon:** +List tabs first, then pass `--page-id` on every command so automation does not +switch or disturb the user's active tab: + ```bash -chrome-devtools start --autoConnect --port 9333 +npx -y @vibebrowser/cli@0.2.12 \ + --remote "$VIBEBROWSER_REMOTE_URL" --json tabs +npx -y @vibebrowser/cli@0.2.12 \ + --remote "$VIBEBROWSER_REMOTE_URL" --page-id --json snapshot ``` ## GitHub Auth From d1071b2a44e70419d30443b76e5704d05487d843 Mon Sep 17 00:00:00 2001 From: Dennis V <2119348+dzianisv@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:49:53 +0000 Subject: [PATCH 5/5] fix(ios): close final release review blockers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/publish-app-store.yml | 27 ++++++++++++++++++++++-- app/(tabs)/settings.tsx | 3 ++- distribution/ios-enrollment-runbook.md | 6 ++++-- docs/applestore.md | 4 ++-- docs/security.md | 2 +- eas.json.README.md | 10 +++++---- src/components/TelemetryConsentModal.tsx | 5 ++--- src/lib/links.ts | 1 + src/lib/sdk.ts | 5 ++++- src/lib/sentry.ts | 3 +++ 10 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/lib/links.ts diff --git a/.github/workflows/publish-app-store.yml b/.github/workflows/publish-app-store.yml index 97e7bdf..82496cc 100644 --- a/.github/workflows/publish-app-store.yml +++ b/.github/workflows/publish-app-store.yml @@ -7,7 +7,9 @@ # ── HUMAN GATE (one-time, after Apple Developer Program enrollment) ────────────── # Complete ALL of the following before releasing. Do NOT invent any of these IDs. # -# 1. Fill and commit the eas.json placeholders (see eas.json.README.md for click paths): +# 1. Link the app to an Expo project and add its UUID as the GitHub Actions +# repository variable EAS_PROJECT_ID (see eas.json.README.md), then fill and +# commit the eas.json placeholders: # submit.production.ios.ascAppId REPLACE_WITH_APP_STORE_CONNECT_APP_ID → numeric App Store Connect App ID # submit.production.ios.appleTeamId REPLACE_WITH_APPLE_TEAM_ID → 10-char Apple Team ID # @@ -26,7 +28,8 @@ # SENTRY_AUTH_TOKEN, SENTRY_ORG, and SENTRY_PROJECT in Expo before releasing. # # Build number is managed remotely by EAS (eas.json: cli.appVersionSource=remote, -# build.production.ios.autoIncrement=buildNumber). No app.json mutation happens here. +# build.production.ios.autoIncrement=buildNumber). The workflow only injects the +# EAS project linkage into its temporary runner copy of app.json. name: Publish to App Store (TestFlight) @@ -54,6 +57,7 @@ jobs: env: EAS_CLI_VERSION: "21.0.0" EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + EAS_PROJECT_ID: ${{ vars.EAS_PROJECT_ID }} steps: - uses: actions/checkout@v6 @@ -80,6 +84,13 @@ jobs: need "APPLE_APP_STORE_CONNECT_API_KEY_ID" "${ASC_KEY_ID:-}" need "APPLE_APP_STORE_CONNECT_ISSUER_ID" "${ASC_ISSUER_ID:-}" need "APPLE_APP_STORE_CONNECT_API_KEY" "${ASC_KEY_B64:-}" + if [ -z "${EAS_PROJECT_ID:-}" ]; then + echo "::error::Missing required repository variable: EAS_PROJECT_ID" + fail=1 + elif ! printf '%s' "$EAS_PROJECT_ID" | grep -Eq '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'; then + echo "::error::EAS_PROJECT_ID must be an Expo project UUID" + fail=1 + fi asc_app_id=$(node -p "require('./eas.json').submit.production.ios.ascAppId || ''") team_id=$(node -p "require('./eas.json').submit.production.ios.appleTeamId || ''") case "$asc_app_id" in @@ -113,6 +124,18 @@ jobs: - name: Install dependencies (deterministic) run: npm ci --legacy-peer-deps + - name: Configure EAS project linkage + run: | + set -euo pipefail + node -e " + const fs = require('fs'); + const j = require('./app.json'); + j.expo.extra = { ...j.expo.extra, eas: { ...j.expo.extra?.eas, projectId: process.env.EAS_PROJECT_ID } }; + fs.writeFileSync('app.json', JSON.stringify(j, null, 2) + '\n'); + " + test "$(node -p "require('./app.json').expo.extra.eas.projectId")" = "$EAS_PROJECT_ID" + echo "Linked build to Expo project $EAS_PROJECT_ID." + - name: Configure App Store Connect API key env: ASC_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }} diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx index f2e412e..d6395e7 100644 --- a/app/(tabs)/settings.tsx +++ b/app/(tabs)/settings.tsx @@ -21,6 +21,7 @@ import { } from "../../src/lib/notifications" import type { Category } from "../../src/lib/notifications" import { hasTelemetryConsent, setTelemetryConsent } from "../../src/lib/telemetry" +import { PRIVACY_POLICY_URL } from "../../src/lib/links" function SettingRow({ icon, @@ -215,7 +216,7 @@ export default function SettingsScreen() { label="Privacy Policy" description="What data we collect and how" isDark={isDark} - onPress={() => Linking.openURL("https://agentlabs.cc/opencode/privacy")} + onPress={() => Linking.openURL(PRIVACY_POLICY_URL)} right={} /> diff --git a/distribution/ios-enrollment-runbook.md b/distribution/ios-enrollment-runbook.md index 08e3bb0..c8f82fc 100644 --- a/distribution/ios-enrollment-runbook.md +++ b/distribution/ios-enrollment-runbook.md @@ -141,13 +141,15 @@ While waiting for Apple's verification call and approval: - Note: Key ID and Issuer ID - Base64-encode the .p8 and store in GitHub secret `APPLE_APP_STORE_CONNECT_API_KEY` -4. Configure the EAS `production` environment for optional crash reporting: +4. Run `eas init` once, then add the generated `extra.eas.projectId` UUID as the GitHub Actions repository variable `EAS_PROJECT_ID`. + +5. Configure the EAS `production` environment for optional crash reporting: - `EXPO_PUBLIC_SENTRY_DSN` - `SENTRY_AUTH_TOKEN` (secret visibility) - `SENTRY_ORG` - `SENTRY_PROJECT` -5. Create an internal TestFlight group and add yourself as tester +6. Create an internal TestFlight group and add yourself as tester --- diff --git a/docs/applestore.md b/docs/applestore.md index fd6a333..61e784d 100644 --- a/docs/applestore.md +++ b/docs/applestore.md @@ -90,7 +90,7 @@ Because the answer is "No", no ERN (Encryption Registration Number) is required | 9 | Export compliance | ✅ Done | `ITSAppUsesNonExemptEncryption: false` added to `app.json`. Answers + rationale in this doc (see Export Compliance section above) and `distribution/app-store-listing.md`. | | 10 | ATS justification in App Review notes | ✅ Done | Full justification text in `distribution/app-store-listing.md` under "App Review Notes — ATS Justification" | | 11 | Reviewer test instructions | ✅ Done | Updated with correct command (`opencode serve --hostname 0.0.0.0`) in `distribution/app-store-listing.md` | -| 12 | GitHub secrets: `EXPO_TOKEN`, `APPLE_APP_STORE_CONNECT_API_KEY_ID`, `APPLE_APP_STORE_CONNECT_ISSUER_ID`, `APPLE_APP_STORE_CONNECT_API_KEY` (base64 .p8) | User | 🟡 post-enrollment — see `.github/workflows/publish-app-store.yml` header | +| 12 | GitHub variable `EAS_PROJECT_ID`; secrets: `EXPO_TOKEN`, `APPLE_APP_STORE_CONNECT_API_KEY_ID`, `APPLE_APP_STORE_CONNECT_ISSUER_ID`, `APPLE_APP_STORE_CONNECT_API_KEY` (base64 .p8) | User | 🟡 post-enrollment — run `eas init`, then see `.github/workflows/publish-app-store.yml` | | 13 | Update `eas.json` placeholders: `ascAppId` + `appleTeamId` | User | 🟡 post-enrollment — see `eas.json.README.md` for click paths | | 14 | CI workflow validated | CI | 🟡 Linux checks pass; PR must prove the macOS Simulator build | | 15 | TestFlight release notes | ✅ Done | `distribution/whatsnew-ios/release-notes-en-US.txt` — polished, 1658 chars (limit 4000) | @@ -102,7 +102,7 @@ Because the answer is "No", no ERN (Encryption Registration Number) is required 1. (manual) Sign in to App Store Connect, create app with bundle id `cc.agentlabs.opencode`. 2. (manual) Generate App Store Connect API key (App Manager role) → download `.p8` → base64 encode → add as GitHub secret. 3. (manual) Update `eas.json` placeholders (Team ID, ASC App ID). -4. (manual) `eas login` + `eas build:configure` for first-time setup (managed signing). +4. (manual) `eas login` + `eas init` + `eas build:configure`; add the generated project UUID as repository variable `EAS_PROJECT_ID`. 5. (automated) Publish a GitHub Release for the version tag → CI calls EAS Build → EAS Submit → IPA lands in TestFlight. 6. (manual, first time) Add internal testers in App Store Connect → distribute via TestFlight. 7. (manual) After internal testing OK → submit for App Store review (production). diff --git a/docs/security.md b/docs/security.md index 2b349f6..0a6cdaa 100644 --- a/docs/security.md +++ b/docs/security.md @@ -152,7 +152,7 @@ When the opencode AI agent requests a file-access permission, the notification b **Files:** `app.json:29`, `app.json:38-39` **Description:** Both platforms allow HTTP connections, which is required for local/LAN servers. This is intentional and correct for the use case. However, neither the Play Store listing, App Store listing, nor a privacy policy document currently explains that HTTP connections may be made to user-provided servers. Google Play's Data Safety section and Apple's App Privacy report will flag arbitrary network access if not documented. **Remediation:** -1. Update the privacy policy at `agentlabs.cc/opencode/privacy` to explain that the app connects to user-configured server addresses that may use HTTP. +1. Update the canonical privacy policy at `https://dzianisv.github.io/opencode-mobile/privacy/` to explain that the app connects to user-configured server addresses that may use HTTP. 2. In Play Store Data Safety: disclose "Other app performance data" collected (crash reports via Sentry — opt-in). **Status:** Open diff --git a/eas.json.README.md b/eas.json.README.md index 5dde065..9aa2943 100644 --- a/eas.json.README.md +++ b/eas.json.README.md @@ -73,10 +73,12 @@ Alternatively, in App Store Connect: ## After filling in the placeholders -1. Commit the updated `eas.json` to the repo. -2. Add the `EXPO_TOKEN` and App Store Connect API GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` for the exact list). -3. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow). -4. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight. +1. Run `eas init` once to create/link the Expo project. +2. Copy the generated `extra.eas.projectId` UUID and add it as the GitHub Actions repository variable `EAS_PROJECT_ID`. The release workflow injects it into `app.json` only on the runner. +3. Commit the updated `eas.json` to the repo. +4. Add the `EXPO_TOKEN` and App Store Connect API GitHub Actions secrets (see `.github/workflows/publish-app-store.yml` for the exact list). +5. Publish a GitHub Release for the version tag (or manually dispatch the App Store workflow). +6. The release event triggers CI to build the IPA via EAS and submit that exact build to TestFlight. --- diff --git a/src/components/TelemetryConsentModal.tsx b/src/components/TelemetryConsentModal.tsx index 28ed48e..bf61b3f 100644 --- a/src/components/TelemetryConsentModal.tsx +++ b/src/components/TelemetryConsentModal.tsx @@ -7,6 +7,7 @@ import { View, Text, TouchableOpacity, StyleSheet, useColorScheme, Modal, Linking } from "react-native" import { Ionicons } from "@expo/vector-icons" +import { PRIVACY_POLICY_URL } from "../lib/links" interface Props { visible: boolean @@ -45,9 +46,7 @@ export function TelemetryConsentModal({ visible, onAllow, onDecline }: Props) { {/* Privacy policy link */} - Linking.openURL("https://agentlabs.cc/opencode/privacy")} - > + Linking.openURL(PRIVACY_POLICY_URL)}> Read our full privacy policy diff --git a/src/lib/links.ts b/src/lib/links.ts new file mode 100644 index 0000000..f22a0f5 --- /dev/null +++ b/src/lib/links.ts @@ -0,0 +1 @@ +export const PRIVACY_POLICY_URL = "https://dzianisv.github.io/opencode-mobile/privacy/" diff --git a/src/lib/sdk.ts b/src/lib/sdk.ts index 0b752fe..cc5b8f9 100644 --- a/src/lib/sdk.ts +++ b/src/lib/sdk.ts @@ -232,7 +232,10 @@ export function createClient(config: ClientConfig) { try { yield JSON.parse(data) } catch (err) { - console.warn("[SSE] Failed to parse event:", data.slice(0, 200), err) + console.warn("[SSE] Failed to parse event", { + length: data.length, + error: err instanceof Error ? err.message : String(err), + }) } } } diff --git a/src/lib/sentry.ts b/src/lib/sentry.ts index 2655c32..6100162 100644 --- a/src/lib/sentry.ts +++ b/src/lib/sentry.ts @@ -50,6 +50,8 @@ export function initSentry() { return scrubEvent(event) }, beforeBreadcrumb(crumb) { + // Console output can contain malformed server payloads, prompts, or code. + if (crumb.category === "console") return null if (crumb.data && typeof crumb.data === "object") { crumb.data = redactObject(crumb.data as Record) } @@ -149,6 +151,7 @@ function scrubEvent(event: T): T { } } if (event.breadcrumbs) { + event.breadcrumbs = event.breadcrumbs.filter((crumb) => crumb.category !== "console") for (const crumb of event.breadcrumbs) { if (typeof crumb.message === "string") crumb.message = redactString(crumb.message) if (crumb.data && typeof crumb.data === "object") {