App Store Submission Readiness (SP2) Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Prepare the iOS + tvOS app for App Store submission up to the "Submit" click — privacy manifest, encryption flag, app-store export config + build script, baseline screenshots, metadata draft, and an App Store Connect checklist — without touching app Swift code.
Architecture: All changes are configuration, scripts, and docs (no Swift logic), so there is zero app-regression surface. The privacy manifest declares exactly the three required-reason APIs the codebase actually uses (audited). A single universal App Store record covers iOS + tvOS (shared bundle id de.jkaindl.physarum). The macOS Developer-ID DMG pipeline is left untouched.
Tech Stack: XcodeGen (apple/PhysarumApp/project.yml), Apple privacy manifest (PrivacyInfo.xcprivacy), xcodebuild archive/-exportArchive, xcrun simctl for screenshots, plist tooling (plutil).
Spec: docs/superpowers/specs/2026-06-21-appstore-submission-readiness-design.md
Conventions¶
- Repo root: the physarum checkout (all paths below are repo-relative); App dir:
apple/PhysarumApp. - Build (macOS) for regression:
cd apple/PhysarumApp && xcodegen generate && xcodebuild -scheme Physarum -destination 'platform=macOS' build. - iOS/tvOS compile:
-destination 'generic/platform=iOS Simulator'/'generic/platform=tvOS Simulator'. - Test target:
PhysarumAppTests(host suite must stay green:xcodebuild test -scheme Physarum -destination 'platform=macOS'). - Commits: Conventional Commits, no emoji, never
--no-verify. Commit per task. Do not push (gated). Work onmain. - No Swift code changes in this plan — only
project.yml, plists, shell/TS scripts, and Markdown docs.
Task 1: Privacy manifest (PrivacyInfo.xcprivacy) + wire into the target¶
Audited required-reason API usage (do not add others without evidence):
- UserDefaults/@AppStorage → NSPrivacyAccessedAPICategoryUserDefaults, reason CA92.1 (app's own data). Used in RootView/AppState/EditorView.
- File timestamp (.contentModificationDateKey in Capture/CaptureCleanup.swift) → NSPrivacyAccessedAPICategoryFileTimestamp, reason C617.1 (timestamps of files inside the app container).
- ProcessInfo.processInfo.systemUptime (Editor/SliderPrecision.swift, Remote/RemoteController.swift) → NSPrivacyAccessedAPICategorySystemBootTime, reason 35F9.1 (measure elapsed time for in-app events).
- Disk space, active keyboard: NOT used → do not declare.
Files:
- Create: apple/PhysarumApp/PrivacyInfo.xcprivacy
- Modify: apple/PhysarumApp/project.yml (add the manifest to the target sources: list, after line ~16 - path: Sources)
- [ ] Step 1: Create the manifest
Create apple/PhysarumApp/PrivacyInfo.xcprivacy:
<?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>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
</dict>
</plist>
- [ ] Step 2: Lint the plist
Run: plutil -lint apple/PhysarumApp/PrivacyInfo.xcprivacy
Expected: … OK.
- [ ] Step 3: Wire it into the target
In apple/PhysarumApp/project.yml, under the app target's sources: list (the one containing - path: Sources), add:
- [ ] Step 4: Regenerate, build, and confirm it lands in the bundle
Run:
cd apple/PhysarumApp && xcodegen generate && xcodebuild -scheme Physarum -destination 'platform=macOS' build
** BUILD SUCCEEDED **. Then locate the built app and confirm the manifest is bundled:
APP=$(find ~/Library/Developer/Xcode/DerivedData -name 'Physarum.app' -path '*/Debug/Physarum.app' 2>/dev/null | head -1)
plutil -p "$APP/Contents/Resources/PrivacyInfo.xcprivacy" 2>/dev/null | head -5 || find "$APP" -name PrivacyInfo.xcprivacy
- [ ] Step 5: iOS + tvOS compile (no break)
Run: xcodebuild -scheme Physarum -destination 'generic/platform=iOS Simulator' build && xcodebuild -scheme Physarum -destination 'generic/platform=tvOS Simulator' build
Expected: ** BUILD SUCCEEDED ** twice.
- [ ] Step 6: Commit
git add apple/PhysarumApp/PrivacyInfo.xcprivacy apple/PhysarumApp/project.yml
git commit -m "feat(appstore): add PrivacyInfo.xcprivacy (UserDefaults, FileTimestamp, SystemBootTime)"
Task 2: Encryption-compliance flag¶
Files:
- Modify: apple/PhysarumApp/project.yml (target info.properties, ~line 31)
- [ ] Step 1: Add the flag
In apple/PhysarumApp/project.yml, under the app target's info: → properties: block (line ~31, where LSApplicationCategoryType/usage strings live), add:
(Rationale: only CryptoKit SHA-256 for content hashing — standard/exempt. This pre-answers the export-compliance question on every submission.)
- [ ] Step 2: Regenerate + build + verify it reaches the built Info.plist
Run:
cd apple/PhysarumApp && xcodegen generate && xcodebuild -scheme Physarum -destination 'platform=macOS' build
APP=$(find ~/Library/Developer/Xcode/DerivedData -name 'Physarum.app' -path '*/Debug/Physarum.app' 2>/dev/null | head -1)
plutil -extract ITSAppUsesNonExemptEncryption raw "$APP/Contents/Info.plist"
** BUILD SUCCEEDED ** and the plutil -extract prints false.
- [ ] Step 3: Commit
git add apple/PhysarumApp/project.yml
git commit -m "feat(appstore): declare ITSAppUsesNonExemptEncryption=false"
Task 3: App-Store export config + build script¶
Files:
- Create: scripts/ExportOptions-appstore.plist
- Create: scripts/build-appstore.sh (executable)
The existing
scripts/ExportOptions.plist(developer-id, macOS DMG) andscripts/build-macos-app.shMUST remain untouched.
- [ ] Step 1: Create the app-store export options
Create scripts/ExportOptions-appstore.plist:
<?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>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>U9X7M39R56</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadSymbols</key>
<true/>
<key>destination</key>
<string>export</string>
</dict>
</plist>
(Note: Xcode 16 renamed app-store → app-store-connect; app-store is still accepted. signingStyle automatic lets Xcode generate the App Store provisioning profile from Johannes' account.)
- [ ] Step 2: Lint the plist
Run: plutil -lint scripts/ExportOptions-appstore.plist
Expected: … OK.
- [ ] Step 3: Create the build script (mirrors build-macos-app.sh structure)
Create scripts/build-appstore.sh:
#!/usr/bin/env bash
# Archives the native Physarum app for iOS + tvOS and exports App Store .ipa packages.
#
# Output: dist-appstore/Physarum-iOS.xcarchive + dist-appstore/ios/*.ipa (and tvos/*.ipa).
# Requires: Xcode (xcodebuild), xcodegen, and an Apple account with App Store
# distribution enabled (Apple Distribution cert + App Store provisioning). The
# archive/export run with -allowProvisioningUpdates so Xcode can create the profile.
#
# The macOS Developer-ID DMG pipeline (build-macos-app.sh) is separate and untouched.
set -euo pipefail
cd "$(dirname "$0")/.."
SCHEME="Physarum"
TEAM_ID="U9X7M39R56"
PROJ_DIR="apple/PhysarumApp"
EXPORT_PLIST_ABS="$(pwd)/scripts/ExportOptions-appstore.plist"
OUT="dist-appstore"
for tool in xcodebuild xcodegen; do
command -v "$tool" >/dev/null 2>&1 || { echo "error: required tool not found: $tool" >&2; exit 1; }
done
[ -f "$EXPORT_PLIST_ABS" ] || { echo "error: missing $EXPORT_PLIST_ABS" >&2; exit 1; }
( cd "$PROJ_DIR" && xcodegen generate )
mkdir -p "$OUT"
archive_and_export() {
local platform="$1" tag="$2"
local archive="$OUT/Physarum-$tag.xcarchive"
echo "==> Archiving $tag ($platform)"
xcodebuild archive \
-project "$PROJ_DIR/Physarum.xcodeproj" \
-scheme "$SCHEME" \
-destination "generic/platform=$platform" \
-archivePath "$archive" \
-allowProvisioningUpdates \
DEVELOPMENT_TEAM="$TEAM_ID"
echo "==> Exporting $tag .ipa"
xcodebuild -exportArchive \
-archivePath "$archive" \
-exportOptionsPlist "$EXPORT_PLIST_ABS" \
-exportPath "$OUT/$tag" \
-allowProvisioningUpdates
}
archive_and_export "iOS" "ios"
archive_and_export "tvOS" "tvos"
echo "Done. Upload the .ipa(s) in $OUT/ via Xcode Organizer or 'xcrun altool/notarytool'/Transporter."
- [ ] Step 4: Make executable + syntax-check (do NOT run a real archive — it needs Johannes' account)
Run:
chmod +x scripts/build-appstore.sh
bash -n scripts/build-appstore.sh && echo "syntax OK"
command -v shellcheck >/dev/null && shellcheck scripts/build-appstore.sh || echo "(shellcheck not installed — skipped)"
syntax OK (shellcheck clean if installed). A full archive is Johannes' step (gated on his App Store distribution setup).
- [ ] Step 5: Commit
git add scripts/ExportOptions-appstore.plist scripts/build-appstore.sh
git commit -m "feat(appstore): app-store export options + iOS/tvOS archive script"
Task 4: Baseline screenshots from the simulator¶
Files:
- Create: scripts/shoot-appstore-screens.sh (executable)
- Output (gitignored build artifacts go to): docs/appstore/screenshots/<device>/
Captures the rendered player hero per required device size. No app code change; the app boots to its first filled curated preset. Richer multi-state shots (editor/library) are Johannes' curation or a later XCUITest follow-up.
- [ ] Step 1: Create the screenshot script
Create scripts/shoot-appstore-screens.sh:
#!/usr/bin/env bash
# Builds Physarum for the simulator and captures App Store baseline screenshots
# (the rendered player hero) for each required device size.
#
# Output: docs/appstore/screenshots/<device>/01-hero.png
# Requires: Xcode + simulators for iPhone 16 Pro Max, iPad Pro 13-inch (M4), Apple TV 4K.
set -euo pipefail
cd "$(dirname "$0")/.."
SCHEME="Physarum"
PROJ="apple/PhysarumApp/Physarum.xcodeproj"
BUNDLE_ID="de.jkaindl.physarum"
OUT="docs/appstore/screenshots"
( cd apple/PhysarumApp && xcodegen generate )
shoot() {
local sim="$1" dir="$2" sdk="$3"
echo "==> $sim"
local udid
# Portable UDID extraction (macOS awk lacks 3-arg match): grep the UUID off the device line.
udid=$(xcrun simctl list devices available | grep -F "$sim (" | grep -oiE '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' | head -1)
[ -n "$udid" ] || { echo "skip: no simulator named '$sim'"; return 0; }
xcrun simctl boot "$udid" 2>/dev/null || true
xcrun simctl bootstatus "$udid" -b
# Build the app for this simulator (into a temp DerivedData so the repo stays clean) and locate the .app
local dd="${TMPDIR:-/tmp}/physarum-dd-$dir"
xcodebuild -project "$PROJ" -scheme "$SCHEME" -destination "id=$udid" -derivedDataPath "$dd" build >/dev/null
local app
app=$(find "$dd/Build/Products" -name 'Physarum.app' -path "*$sdk*" | head -1)
[ -n "$app" ] || { echo "skip: no .app built for $sim"; return 0; }
xcrun simctl install "$udid" "$app"
xcrun simctl launch "$udid" "$BUNDLE_ID"
sleep 8 # let the Metal renderer warm up and the pattern form
mkdir -p "$OUT/$dir"
xcrun simctl io "$udid" screenshot "$OUT/$dir/01-hero.png"
echo "wrote $OUT/$dir/01-hero.png"
}
shoot "iPhone 16 Pro Max" "iphone-6.9" "iphonesimulator"
shoot "iPad Pro 13-inch (M4)" "ipad-13" "iphonesimulator"
shoot "Apple TV 4K" "appletv" "appletvsimulator"
echo "Done. Review docs/appstore/screenshots/*/01-hero.png (replace/add as desired)."
- [ ] Step 2: Make executable + syntax-check
Run: chmod +x scripts/shoot-appstore-screens.sh && bash -n scripts/shoot-appstore-screens.sh && echo "syntax OK"
Expected: syntax OK.
- [ ] Step 3: Run it + verify the PNGs are non-empty and rendered (not black)
Run: ./scripts/shoot-appstore-screens.sh
Then verify each produced PNG exists and has plausible size (a black/empty frame is a failure):
sleep or capture on a real device (Johannes).
- [ ] Step 4: Add a .gitignore note OR commit the baseline PNGs
Decide: commit the baseline PNGs (so they're versioned) — they are small and useful. Ensure they are tracked:
- [ ] Step 5: Commit
Task 5: Metadata draft, privacy labels, and ASC checklist¶
Files:
- Create: docs/appstore/metadata.md
- Create: docs/appstore/privacy-labels.md
- Create: docs/appstore/ASC-submission-checklist.md
- [ ] Step 1: Metadata draft
Create docs/appstore/metadata.md (Johannes finalizes; the App Store name must be globally unique — verify in ASC):
# App Store metadata — Physarum (draft)
- **Name:** Physarum *(if taken in ASC, try "Physarum — Living Ink" / "Physarum Ambient")*
- **Subtitle (≤30):** Living, audio-reactive art
- **Category:** Entertainment (Primary). Secondary: Graphics & Design (optional)
- **Age rating:** 4+
- **Price:** Free
- **Promotional text (≤170):** A mesmerizing slime-mold simulation you can shape and play — audio-reactive, endlessly evolving, beautiful on iPhone, iPad, and the big screen via Apple TV.
- **Keywords (≤100, comma-separated):** generative,art,slime mold,physarum,audio reactive,visualizer,ambient,simulation,creative,wallpaper,relaxing,apple tv
- **Description:**
Physarum turns the emergent behaviour of slime mold into living, breathing art.
Thousands of agents lay down and follow trails, and from those simple rules
intricate, ever-shifting patterns emerge. Shape them with a full parameter
editor, let them react to music through your microphone, and capture stills,
videos, or Live Photos. On Apple TV, Physarum becomes a lean-back ambient
display you steer from the couch or mirror live from your phone.
- Full authoring editor: tune agents, sensing, deposition, decay, colour, post-FX
- Audio-reactive: the visuals pulse and bloom to sound
- Capture: high-res stills, video clips, Live Photos
- Apple TV ambient mode with phone control
- Curated presets to start from; save your own
- **Support URL:** https://git.jkaindl.de/jkaindl/physarum *(or a dedicated support page)*
- **Marketing URL:** https://uplink.jkaindl.de *(optional)*
- **Copyright:** © 2026 Johannes Kaindl
- [ ] Step 2: Privacy nutrition labels
Create docs/appstore/privacy-labels.md:
# App Privacy answers (ASC "App Privacy" questionnaire)
**Summary: Data Not Collected.** Physarum does not collect or transmit any user data.
- **Tracking:** No. (Manifest: NSPrivacyTracking=false, no tracking domains.)
- **Microphone:** Audio is analysed on-device into frequency bands to drive the
visuals in real time. It is never recorded, stored, or transmitted. → not "collected".
- **Photos:** Add-only — the app writes captured images/clips to the user's photo
library on request. It never reads or collects photos. → not "collected".
- **Local network / Bonjour:** Used only to connect to the user's own Apple TV on
the local network for screen control. No data leaves the local network; nothing
collected.
- **Required-reason APIs (declared in PrivacyInfo.xcprivacy):** UserDefaults (CA92.1,
app's own settings), File timestamp (C617.1, pruning the app's own temp files),
System boot time (35F9.1, measuring elapsed time for in-app throttling).
In ASC, answer "No, we do not collect data from this app" for every category.
- [ ] Step 3: ASC submission checklist
Create docs/appstore/ASC-submission-checklist.md:
# App Store Connect submission checklist — iOS + tvOS (Johannes' steps)
Prepared by SP2. The artifacts (privacy manifest, encryption flag, export config,
build script, screenshots, metadata, privacy answers) are in the repo. These are
the account-bound steps that must be done in App Store Connect / Xcode.
## One-time account setup
- [ ] Apple Developer Program active (Team U9X7M39R56).
- [ ] Apple Distribution certificate present (Xcode → Settings → Accounts → Manage Certificates → +).
- [ ] (Free app) Confirm the Free Apps agreement is active in ASC → Agreements, Tax, and Banking.
## Create the app record (ONE universal record for iOS + tvOS)
- [ ] ASC → Apps → + → New App. Platforms: iOS **and** tvOS. Bundle ID: `de.jkaindl.physarum`.
If "Physarum" is taken, pick an alternative name (see metadata.md) — the bundle id stays.
- [ ] Set Primary category Entertainment, Age rating 4+ (questionnaire: no objectionable content), Price Free.
## Build + upload
- [ ] Run `PHYSARUM... ./scripts/build-appstore.sh` (or archive in Xcode: Product → Archive for iOS, then tvOS).
- [ ] Upload each .ipa via Xcode Organizer → Distribute App → App Store Connect (or Transporter).
- [ ] Wait for processing; select the build for each platform in ASC.
## Listing
- [ ] Paste name/subtitle/keywords/description/promo from `docs/appstore/metadata.md`.
- [ ] Upload screenshots from `docs/appstore/screenshots/` (iPhone 6.9", iPad 13", tvOS) — replace/add as desired.
- [ ] App Privacy: answer "Data Not Collected" per `docs/appstore/privacy-labels.md`.
- [ ] Support URL + (optional) Marketing URL.
## Submit
- [ ] Add a reviewer note if helpful (mic = audio-reactive visuals; local network = Apple TV control).
- [ ] Submit for Review (both platforms in the one submission).
## Notes / gotchas
- iOS + tvOS share one record because they share the bundle id `de.jkaindl.physarum`.
- macOS is NOT in the App Store — it ships as the notarized Developer-ID DMG (unchanged).
- Export compliance is pre-answered via ITSAppUsesNonExemptEncryption=false.
- [ ] Step 4: Lint the docs exist + commit
Run: ls -1 docs/appstore/*.md
Expected: the three files listed.
git add docs/appstore/metadata.md docs/appstore/privacy-labels.md docs/appstore/ASC-submission-checklist.md
git commit -m "docs(appstore): metadata draft, privacy labels, ASC submission checklist"
Task 6: Final regression + summary¶
- [ ] Step 1: Regenerate + build all three platforms
Run:
cd apple/PhysarumApp && xcodegen generate \
&& xcodebuild -scheme Physarum -destination 'platform=macOS' build \
&& xcodebuild -scheme Physarum -destination 'generic/platform=iOS Simulator' build \
&& xcodebuild -scheme Physarum -destination 'generic/platform=tvOS Simulator' build
** BUILD SUCCEEDED ** three times (the manifest + encryption flag must not break any platform).
- [ ] Step 2: Host suite (no regression — SP1 stays green)
Run: xcodebuild test -scheme Physarum -destination 'platform=macOS'
Expected: ** TEST SUCCEEDED **, 293 tests, 0 failures.
- [ ] Step 3: Confirm the developer-id macOS pipeline is untouched
Run: git diff --name-only bf70584..HEAD -- scripts/ExportOptions.plist scripts/build-macos-app.sh apple/PhysarumApp/Physarum.macOS.entitlements
Expected: empty (none of the macOS-DMG pipeline files changed).
- [ ] Step 4: Done
No commit needed if Steps 1-3 are clean (verification only). SP2 deliverables are complete; the remaining work is Johannes' account-bound ASC steps in docs/appstore/ASC-submission-checklist.md.
Done criteria¶
- [ ]
PrivacyInfo.xcprivacybundled, valid, declares exactly the 3 audited APIs; iOS+tvOS build green. - [ ]
ITSAppUsesNonExemptEncryption=falsein the built Info.plist. - [ ]
ExportOptions-appstore.plist+build-appstore.shcreated (lint/syntax clean); developer-id pipeline untouched. - [ ] Baseline screenshots produced for the available required device sizes (non-black).
- [ ]
metadata.md+privacy-labels.md+ASC-submission-checklist.mdwritten. - [ ] 3-platform build + host suite still green (zero regression).