Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
651c241
iOS: public App Store lane (com.cmux.app), privacy manifest, fastlane…
lawrencecchen Jun 23, 2026
e240275
ios-screenshots: read SNAPSHOT_DEVICES from GITHUB_ENV (set -u fix)
lawrencecchen Jun 23, 2026
7134e05
ios-screenshots: pick devices from available simulators, skip RAM var…
lawrencecchen Jun 23, 2026
94a3f29
ios-screenshots: drive devices/languages from Fastfile, not Snapfile
lawrencecchen Jun 23, 2026
2fce840
ios-screenshots: capture via standalone preview screens (real images)
lawrencecchen Jun 23, 2026
914de8b
ios-screenshots: populated terminal, clean status bar, dismiss banner
lawrencecchen Jun 23, 2026
0f928d5
ios-screenshots: feed terminal sample content on didResize
lawrencecchen Jun 23, 2026
98d58b5
iOS: localize app into 12 more languages (machine translation)
lawrencecchen Jun 23, 2026
a5342f9
ios: localized App Store listing metadata (14 locales)
lawrencecchen Jun 24, 2026
94898be
ios-screenshots: realistic framed shots (agents, keyboard, notificati…
lawrencecchen Jun 24, 2026
b3be04d
ci: re-trigger screenshots workflow
lawrencecchen Jun 24, 2026
d3a23e2
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jun 24, 2026
0b9930e
ios-screenshots: fix MainActor setupSnapshot call; add localized fram…
lawrencecchen Jun 24, 2026
2d70f35
ios-screenshots: real frames (frameit) + real recorded agent TUIs
lawrencecchen Jun 24, 2026
42bb732
ios-screenshots: grid probe + font override + 39-locale titles
lawrencecchen Jun 24, 2026
82c69c3
ios-screenshots: auto-fit terminal width, real 76-col agent sessions,…
lawrencecchen Jun 24, 2026
8141521
ios-screenshots: generate HTML gallery in CI (screenshots/preview/ind…
lawrencecchen Jun 24, 2026
ae40c0e
ios-screenshots: install Pillow in CI for the iPad compositor
lawrencecchen Jun 24, 2026
4ace31a
ios-screenshots: iPad compositor via ImageMagick (fast, no Pillow)
lawrencecchen Jun 24, 2026
4a3139a
ios-screenshots: gallery at screenshots root with subdir+URL-encoded …
lawrencecchen Jun 24, 2026
654d95f
ios-screenshots: premium composition (stitch real frame, large device…
lawrencecchen Jun 24, 2026
dc35f40
ios-screenshots: SF Pro header + nature backgrounds
lawrencecchen Jun 24, 2026
91c788b
ios-screenshots: round capture corners before stitching (clean frame …
lawrencecchen Jun 24, 2026
e208826
ios screenshots: opening-mask frame stitch + agent logos in headers
lawrencecchen Jun 25, 2026
633bd59
ios screenshots: real terminal titlebar, smaller inline logos, bright…
lawrencecchen Jun 25, 2026
e6c5cd4
ios screenshots: unified Dynamic Island, per-screen bg variation, raw…
lawrencecchen Jun 25, 2026
2ff4250
ios screenshots: terminal preview fills header with terminal color, r…
lawrencecchen Jun 25, 2026
0775c11
ios screenshots: glass-pill nav title in terminal preview
lawrencecchen Jun 25, 2026
9eb533d
ios screenshots: bigger device, smaller title, horizon-free backgrounds
lawrencecchen Jun 25, 2026
0373057
ios screenshots: strip OpenCode's explicit bg so it renders uniformly
lawrencecchen Jun 25, 2026
232bc55
ios screenshots: OpenCode keeps its dark card, made uniform (no olive…
lawrencecchen Jun 25, 2026
ab305c9
ios screenshots: render OpenCode on a matching #0a0a0a terminal backg…
lawrencecchen Jun 26, 2026
0b54405
ios screenshots: re-record OpenCode with a full-screen response
lawrencecchen Jun 26, 2026
c85b09d
ios screenshots: real notification (not a drawn banner)
lawrencecchen Jun 26, 2026
81617d6
ios screenshots: auto-derive terminal bg from transcript (no hardcode…
lawrencecchen Jun 26, 2026
dc1456f
ios screenshots: capture the real notification banner at fixed timing
lawrencecchen Jun 26, 2026
8821c1c
ios screenshots: real Mac-streamed capture orchestration (WIP)
lawrencecchen Jun 26, 2026
68971db
ci: Blacksmith streamed-screenshot validation workflow (WIP, manual)
lawrencecchen Jun 26, 2026
c49d700
ci: trigger streamed-validate via push-to-self on the feature branch
lawrencecchen Jun 26, 2026
81fc984
ci: streamed-validate stands up dev web backend (Postgres + Next.js) …
lawrencecchen Jun 26, 2026
0e9e5d7
ci: streamed-validate uses native Postgres + next dev (no Docker)
lawrencecchen Jun 26, 2026
6850b67
ci: detach web server with nohup (macOS has no setsid)
lawrencecchen Jun 27, 2026
6c09a33
ci: run streamed-validate on GitHub-hosted macos-26 (Blacksmith queue…
lawrencecchen Jun 28, 2026
f247cbd
ios-streamed-validate: run on Blacksmith macos-26, not paid GitHub-ho…
lawrencecchen Jun 28, 2026
e874a7f
ios-streamed-validate: pin Blacksmith macos-26 + Xcode 26 (not Depot/…
lawrencecchen Jun 28, 2026
192b37f
ios-streamed-validate: mirror the Blacksmith fleet build recipe (fix …
lawrencecchen Jun 28, 2026
23a5a76
ios-streamed-validate: make the streamed capture actually run (idb, s…
lawrencecchen Jun 28, 2026
3984f35
ios-streamed-validate: capture device-state diagnostics on capture fa…
lawrencecchen Jun 28, 2026
f89dc2f
ios-streamed-validate: install idb_companion (facebook/fb tap) + /usr…
lawrencecchen Jun 28, 2026
c656c82
ios-streamed-validate: real claude agent content in the streamed shot
lawrencecchen Jun 30, 2026
f8bf46d
ios-streamed-validate: trust claude sandbox by realpath (/private/tmp)
lawrencecchen Jun 30, 2026
8d2b892
ios-streamed-validate: test the DeepSeek-backed agent path (pi)
lawrencecchen Jun 30, 2026
4bb8f38
ios-streamed-validate: replay recorded agent sessions (claude) instea…
lawrencecchen Jun 30, 2026
a2d3640
ios-streamed-validate: resume claude by session id, seed both cwd enc…
lawrencecchen Jun 30, 2026
d42e902
ios-streamed-validate: real recorded sessions for all 4 agents (claud…
lawrencecchen Jul 1, 2026
cef5511
ios-streamed-validate: reliable back-navigation between agent captures
lawrencecchen Jul 1, 2026
335a287
ios-streamed-validate: give codex a DeepSeek provider so resume rende…
lawrencecchen Jul 1, 2026
d770c40
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jul 1, 2026
969cca8
capture-streamed: screencapture the Mac per agent (theme diagnostic)
lawrencecchen Jul 1, 2026
51d3c87
streamed shots: force dark theme on CI Mac + codex press-enter nudge
lawrencecchen Jul 1, 2026
9127e2f
streamed shots: codex uses fake OpenAI auth (real codex, no DeepSeek)
lawrencecchen Jul 1, 2026
806ee4f
streamed shots: drop DeepSeek, fake per-provider auth so each agent s…
lawrencecchen Jul 1, 2026
f60d73d
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jul 1, 2026
9854b9d
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jul 2, 2026
019458f
capture-streamed: set terminal font once up front (fix OpenCode botto…
lawrencecchen Jul 3, 2026
fe3b0e6
capture-streamed: per-agent set_font + long settle (consistent size A…
lawrencecchen Jul 3, 2026
d1d4cb7
capture-streamed: log each agent's Mac PTY grid (diagnose OpenCode sc…
lawrencecchen Jul 3, 2026
2a36c2b
capture-streamed: smaller font for OpenCode so it matches the others'…
lawrencecchen Jul 3, 2026
216598a
streamed shots: opencode layout=stretch (full width), revert font hack
lawrencecchen Jul 3, 2026
3988950
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jul 4, 2026
be5be9f
iOS streamed capture: claude theme dark-ansi so it renders on cmux Mo…
lawrencecchen Jul 4, 2026
694d57d
iOS: add dispatch-only App Store upload workflow + --marketing-version
lawrencecchen Jul 4, 2026
9dbb52a
screenshots: drop pi from OpenCode header (pi has its own shot)
lawrencecchen Jul 4, 2026
8f61bfb
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jul 9, 2026
11a1323
Fix App Store lane alias guard
lawrencecchen Jul 9, 2026
0ead529
Address screenshot preview policy findings
lawrencecchen Jul 9, 2026
94014d9
Localize screenshot notification fixture
lawrencecchen Jul 9, 2026
16c485c
Limit app-declared iOS locales
lawrencecchen Jul 9, 2026
aa28a59
Guard screenshot workflow secrets
lawrencecchen Jul 9, 2026
3ed49c8
Address App Store screenshot PR feedback
lawrencecchen Jul 9, 2026
35bb4ac
Fix streamed simulator lookup
lawrencecchen Jul 9, 2026
ae22180
Handle localized notification permission in screenshots
lawrencecchen Jul 9, 2026
4692116
Fix screenshot preview task modifier
lawrencecchen Jul 9, 2026
090ff08
Address remaining screenshot review feedback
lawrencecchen Jul 9, 2026
33410ea
Fix terminal preview keyboard height parse
lawrencecchen Jul 9, 2026
d0f3a4f
Restrict streamed validation secrets to main
lawrencecchen Jul 9, 2026
32ff0e4
Preserve raw App Store screenshot captures
lawrencecchen Jul 14, 2026
ea1ef25
Merge origin/main into feat-ios-appstore-prod-lane
lawrencecchen Jul 14, 2026
902a1da
Merge remote-tracking branch 'origin/main' into feat-ios-appstore-pro…
lawrencecchen Jul 14, 2026
8e690b3
Parallelize App Store screenshot framing
lawrencecchen Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions .github/workflows/ios-screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
name: iOS App Store screenshots

# Captures fastlane screenshots for the public App Store listing (com.cmux.app)
# on the required device classes — 6.9" iPhone + 13" iPad — in every locale the
# app is localized in (en-US + ja). The capture runs against a DEBUG build with
# the CMUX_UITEST_MOCK_DATA hook, so no Apple signing is needed to shoot; only
# the optional upload-to-ASC step needs the App Store Connect API key.
#
# On PRs touching the screenshot tooling it runs capture-only (validates the
# snapshot test + Snapfile). Upload to App Store Connect is workflow_dispatch +
# upload=true only.

on:
workflow_dispatch:
inputs:
ref:
description: Branch or SHA to capture from
required: false
default: ""
languages:
description: "Comma-separated App Store locales (app is localized in en-US, ja)"
required: false
default: "en-US,ja"
upload:
description: "Upload captured screenshots to the com.cmux.app App Store listing"
required: false
default: false
type: boolean
pull_request:
paths:
- "ios/fastlane/**"
- "ios/cmuxUITests/SnapshotUITests.swift"
- "ios/cmuxUITests/SnapshotHelper.swift"
- "ios/Gemfile"
- ".github/workflows/ios-screenshots.yml"

permissions:
contents: read
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

jobs:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
screenshots:
# Same lane as the iOS simulator tests: screenshots run inside the Simulator,
# not as a foregrounded Mac app, so the Blacksmith foreground limitation does
# not apply.
runs-on: ${{ vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26' }}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.ref }}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
persist-credentials: false
submodules: recursive

- name: Select Xcode
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
XCODE_DIR="/Applications/Xcode.app/Contents/Developer"
else
XCODE_APP="$(find /Applications -maxdepth 1 -type d -name 'Xcode*.app' -print 2>/dev/null | sort | tail -n 1 || true)"
[ -n "$XCODE_APP" ] || { echo "No Xcode.app found" >&2; exit 1; }
XCODE_DIR="$XCODE_APP/Contents/Developer"
fi
echo "DEVELOPER_DIR=$XCODE_DIR" >> "$GITHUB_ENV"
export DEVELOPER_DIR="$XCODE_DIR"
xcodebuild -version
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

- name: Install zig
run: ./scripts/install-zig-ci.sh

- name: Ensure iOS simulator runtime
run: |
set -euo pipefail
if xcrun simctl list runtimes available | grep -Eq '\biOS\b'; then exit 0; fi
echo "No available iOS simulator runtime; downloading the iOS platform."
xcodebuild -downloadPlatform iOS
xcrun simctl list runtimes available | grep -Eq '\biOS\b'

- name: Provision GhosttyKit
run: ./scripts/ensure-ghosttykit.sh

- name: Resolve screenshot devices (6.9" iPhone + 13" iPad)
run: |
set -euo pipefail
# Device model names drift between Xcode versions, so resolve the
# required App Store classes at runtime instead of pinning. Pick from
# the AVAILABLE SIMULATORS (not device types) so fastlane finds an
# existing simulator, and skip RAM-variant duplicates like
# "iPad Pro 13-inch (M5) (16GB)" which have no pre-created simulator.
python3 - <<'PY' >> "$GITHUB_ENV"
import json, re, subprocess

def simctl(*args):
return json.loads(subprocess.check_output(["xcrun", "simctl", "list", *args, "-j"]))

def vkey(s):
return [int(x) for x in re.findall(r"\d+", s or "")] or [0]

data = simctl("devices", "available")
names = sorted({
d["name"]
for runtime in data.get("devices", {}).values()
for d in runtime
if d.get("isAvailable", True)
})

def pick(patterns, fallbacks):
for pat in patterns:
hits = sorted([n for n in names if re.fullmatch(pat, n)], key=vkey)
if hits:
return hits[-1]
for f in fallbacks:
hits = sorted([n for n in names if f(n)], key=vkey)
if hits:
return hits[-1]
return None

# 6.9" iPhone (Pro Max) and 13" iPad (Pro, else Air). "GB" filters out
# RAM-variant names that are not instantiated as simulators.
iphone = pick([r"iPhone \d+ Pro Max"],
[lambda n: n.startswith("iPhone") and "Pro Max" in n and "GB" not in n,
lambda n: n.startswith("iPhone") and "GB" not in n])
ipad = pick([r"iPad Pro 13-inch \(M\d+\)", r"iPad Air 13-inch \(M\d+\)"],
[lambda n: n.startswith("iPad") and "13-inch" in n and "GB" not in n,
lambda n: n.startswith("iPad") and "GB" not in n])
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
if not iphone or not ipad:
raise SystemExit("Could not resolve iPhone/iPad from available simulators: " + ", ".join(names))
print(f"SNAPSHOT_DEVICES={iphone},{ipad}")
PY
# $SNAPSHOT_DEVICES is only in $GITHUB_ENV for later steps, not this
# shell, so read it back from the file rather than referencing the var
# (which would trip `set -u`).
grep '^SNAPSHOT_DEVICES=' "$GITHUB_ENV" || { echo "device resolution failed" >&2; exit 1; }

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- name: Ensure fastlane
run: |
set -euo pipefail
# Use the Homebrew fastlane (ships its own Ruby) instead of bundler:
# macOS system Ruby is 2.6 but fastlane needs >= 3.0, so `bundle install`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 $SNAPSHOT_DEVICES is always empty at this echo

The Python heredoc writes SNAPSHOT_DEVICES=… to $GITHUB_ENV, which only takes effect in subsequent steps — not in the current step's shell. The echo "Resolved SNAPSHOT_DEVICES=$SNAPSHOT_DEVICES" line therefore always prints "Resolved SNAPSHOT_DEVICES=" and provides no useful diagnostic. The fastlane screenshots step in the next step correctly receives the variable, so there is no functional breakage, but the misleading output could confuse anyone debugging a CI capture failure.

# against system Ruby can't resolve. This matches the local dev setup.
if ! command -v fastlane >/dev/null 2>&1; then
brew install fastlane
fi
fastlane --version

- name: Capture screenshots
working-directory: ios
env:
SNAPSHOT_LANGUAGES: ${{ inputs.languages || 'en-US,ja' }}
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_HIDE_CHANGELOG: "1"
run: |
set -euo pipefail
echo "Devices: $SNAPSHOT_DEVICES"
echo "Languages: $SNAPSHOT_LANGUAGES"
fastlane screenshots
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

- name: Upload screenshots artifact
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-appstore-screenshots
path: ios/fastlane/screenshots
if-no-files-found: warn
retention-days: 14

- name: Materialize App Store Connect API key
if: ${{ github.event_name == 'workflow_dispatch' && inputs.upload }}
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_KEY_P8_BASE64: ${{ secrets.ASC_API_KEY_P8_BASE64 }}
run: |
set -euo pipefail
if [ -z "${ASC_API_KEY_ID:-}" ] || [ -z "${ASC_API_ISSUER_ID:-}" ] || [ -z "${ASC_API_KEY_P8_BASE64:-}" ]; then
echo "Missing ASC_API_KEY_ID / ASC_API_ISSUER_ID / ASC_API_KEY_P8_BASE64 secrets" >&2
exit 1
fi
KEY_DIR="$HOME/.appstoreconnect/private_keys"
mkdir -p "$KEY_DIR"
printf '%s' "$ASC_API_KEY_P8_BASE64" | base64 --decode > "$KEY_DIR/AuthKey_${ASC_API_KEY_ID}.p8"
chmod 600 "$KEY_DIR/AuthKey_${ASC_API_KEY_ID}.p8"
{
echo "ASC_API_KEY_ID=$ASC_API_KEY_ID"
echo "ASC_API_ISSUER_ID=$ASC_API_ISSUER_ID"
echo "ASC_API_KEY_PATH=$KEY_DIR/AuthKey_${ASC_API_KEY_ID}.p8"
} >> "$GITHUB_ENV"

- name: Upload screenshots to App Store Connect
if: ${{ github.event_name == 'workflow_dispatch' && inputs.upload }}
working-directory: ios
env:
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_HIDE_CHANGELOG: "1"
run: fastlane upload_screenshots
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public enum MobileBuildType: String, Equatable, Sendable {
///
/// `#if DEBUG` short-circuits to ``dev`` so a local build is never mistaken
/// for a distribution build. In Release the bundle id decides: the beta
/// TestFlight bundle is `dev.cmux.app.beta`; anything else is treated as
/// ``prod``.
/// TestFlight bundle is `dev.cmux.app.beta`; the public App Store bundle is
/// `com.cmux.app`. Anything else in Release is also treated as ``prod``.
///
/// - Parameters:
/// - isDebugBuild: `true` when compiled with `DEBUG` defined. Injected so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ struct MobileFeedbackRouteTests {
}

@Test func releaseNonBetaBundleIsProd() {
// com.cmux.app is the public App Store bundle id.
#expect(MobileBuildType.resolve(isDebugBuild: false, bundleIdentifier: "com.cmux.app") == .prod)
#expect(MobileBuildType.resolve(isDebugBuild: false, bundleIdentifier: "dev.cmux.app") == .prod)
#expect(MobileBuildType.resolve(isDebugBuild: false, bundleIdentifier: nil) == .prod)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,50 @@ private struct TerminalLayoutPreviewSurface: UIViewRepresentable {
return view
}

func updateUIView(_ uiView: UIView, context: Context) {}
// Feed a sample agent-session transcript once the surface has a real grid
// size, so App Store screenshots show a populated terminal instead of a
// blank one. Gated on CMUX_UITEST_TERMINAL_PREVIEW_CONTENT=1 so the
// blank-layout preview used by geometry tests is unchanged.
func updateUIView(_ uiView: UIView, context: Context) {
guard ProcessInfo.processInfo.environment["CMUX_UITEST_TERMINAL_PREVIEW_CONTENT"] == "1",
!context.coordinator.didFeedContent,
let surface = uiView as? GhosttySurfaceView,
uiView.bounds.width > 1, uiView.bounds.height > 1
else { return }
context.coordinator.didFeedContent = true
surface.processOutput(Self.sampleTranscript)
}

/// A short, realistic coding-agent session (ANSI-colored) for screenshots.
private static let sampleTranscript: Data = {
let esc = "\u{1B}"
let reset = "\(esc)[0m"
let dim = "\(esc)[2m"
let green = "\(esc)[1;32m"
let magenta = "\(esc)[1;35m"
let cyan = "\(esc)[36m"
let prompt = "\(green)❯\(reset)"
let lines = [
"\(dim)~/projects/app\(reset) \(cyan)main\(reset)",
"\(prompt) claude \(dim)\"add a dark mode toggle\"\(reset)",
"",
"\(magenta)●\(reset) I'll add a dark mode toggle to Settings.",
" \(dim)Reading\(reset) SettingsView.swift",
" \(green)✓\(reset) Added \(cyan)@AppStorage(\"isDarkMode\")\(reset)",
" \(green)✓\(reset) Wired \(cyan)Toggle\(reset) into the General section",
" \(green)✓\(reset) Applied \(cyan).preferredColorScheme\(reset)",
"",
" \(green)Build succeeded.\(reset) 2 files changed.",
"",
"\(prompt) ",
]
return Data(lines.joined(separator: "\r\n").utf8)
}()

/// Retained delegate (the surface holds it weakly). No-op: the preview only
/// exercises layout, not input/resize round-trips.
final class Coordinator: GhosttySurfaceViewDelegate {
var didFeedContent = false
func ghosttySurfaceView(_ surfaceView: GhosttySurfaceView, didProduceInput data: Data) {}
func ghosttySurfaceView(_ surfaceView: GhosttySurfaceView, didResize size: TerminalGridSize) {}
}
Expand Down
12 changes: 12 additions & 0 deletions ios/cmux-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
8B8A10022DF5000000A66F90 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B8A10192DF5000000A66F90 /* Assets.xcassets */; };
8B8A10032DF5000000A66F90 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8B8A101A2DF5000000A66F90 /* InfoPlist.xcstrings */; };
8B8A10042DF5000000A66F90 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8B8A101B2DF5000000A66F90 /* Localizable.xcstrings */; };
8B8A10E02DF5000000A66F90 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8B8A10E12DF5000000A66F90 /* PrivacyInfo.xcprivacy */; };
8B8A10052DF5000000A66F90 /* cmuxUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B8A101C2DF5000000A66F90 /* cmuxUITests.swift */; };
8B8A10E22DF5000000A66F90 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B8A10E32DF5000000A66F90 /* SnapshotHelper.swift */; };
8B8A10E42DF5000000A66F90 /* SnapshotUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B8A10E52DF5000000A66F90 /* SnapshotUITests.swift */; };
8B41F6832DEDD23B001A66F9 /* cmuxFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 8B41F6822DEDD23B001A66F9 /* cmuxFeature */; };
8B41F6852DEDD25C001A66F9 /* cmuxFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 8B41F6842DEDD25C001A66F9 /* cmuxFeature */; };
8B7E20012DF3A00100A66F90 /* CMUXMobileCore in Frameworks */ = {isa = PBXBuildFile; productRef = 8B7E20032DF3A00100A66F90 /* CMUXMobileCore */; };
Expand Down Expand Up @@ -47,7 +50,10 @@
8B8A10192DF5000000A66F90 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
8B8A101A2DF5000000A66F90 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
8B8A101B2DF5000000A66F90 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
8B8A10E12DF5000000A66F90 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
8B8A101C2DF5000000A66F90 /* cmuxUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmuxUITests.swift; sourceTree = "<group>"; };
8B8A10E32DF5000000A66F90 /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; };
8B8A10E52DF5000000A66F90 /* SnapshotUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotUITests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -77,6 +83,7 @@
isa = PBXGroup;
children = (
8B8A10192DF5000000A66F90 /* Assets.xcassets */,
8B8A10E12DF5000000A66F90 /* PrivacyInfo.xcprivacy */,
8B8A10212DF5000000A66F90 /* Resources */,
8B8A10172DF5000000A66F90 /* cmuxApp.swift */,
8B8A10F12DF5000000A66F90 /* CmuxAppDelegate.swift */,
Expand All @@ -90,6 +97,8 @@
isa = PBXGroup;
children = (
8B8A101C2DF5000000A66F90 /* cmuxUITests.swift */,
8B8A10E52DF5000000A66F90 /* SnapshotUITests.swift */,
8B8A10E32DF5000000A66F90 /* SnapshotHelper.swift */,
);
path = cmuxUITests;
sourceTree = "<group>";
Expand Down Expand Up @@ -237,6 +246,7 @@
8B8A10022DF5000000A66F90 /* Assets.xcassets in Resources */,
8B8A10032DF5000000A66F90 /* InfoPlist.xcstrings in Resources */,
8B8A10042DF5000000A66F90 /* Localizable.xcstrings in Resources */,
8B8A10E02DF5000000A66F90 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -265,6 +275,8 @@
buildActionMask = 2147483647;
files = (
8B8A10052DF5000000A66F90 /* cmuxUITests.swift in Sources */,
8B8A10E42DF5000000A66F90 /* SnapshotUITests.swift in Sources */,
8B8A10E22DF5000000A66F90 /* SnapshotHelper.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
56 changes: 56 additions & 0 deletions ios/cmux/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- cmux does not track users across apps/websites and embeds no ad/tracking SDKs. -->
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>

<!-- First-party data collection. Crash/diagnostics SDKs (e.g. Sentry) are not
linked into the iOS app; analytics are first-party product-interaction
events sent to the cmux web API (server-side PostHog proxy), gated by an
in-app opt-out. Keep in lockstep with the App Store Connect privacy label. -->
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeProductInteraction</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>

<!-- Required-reason API declarations for first-party usage only. -->
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<!-- App preferences, onboarding state, push-registration opt-in, and
device-registry persistence via UserDefaults / @AppStorage. -->
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<!-- Reads the app's own bundle executable modification date for the
in-app diagnostic build stamp. -->
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>DDA9.1</string>
</array>
</dict>
</array>
</dict>
</plist>
Loading
Loading