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] 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") {