Files
punktfunk/clients/apple/store/review-notes.md
enricobuehler 0890cf3244
apple / swift (pull_request) Successful in 1m22s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m16s
ci / rust-arm64 (pull_request) Successful in 2m13s
ci / docs-site (pull_request) Successful in 1m47s
ci / rust (pull_request) Successful in 7m49s
docs(apple/store): App Store copy for iOS, macOS and tvOS, counted against Apple's limits
German-first Promotional Text, Descriptions, Keywords and App Review notes,
plus the app-specific privacy text the existing website policy is missing.

Every character-limited field is checked by check-limits.py, which also catches
headings whose stated count has drifted from the real length. Three things the
brief assumed turned out not to hold, and the copy says so rather than shipping
the claim: a Mac cannot act as a host, the published privacy policy covers only
the website, and the App Review notes field caps at 4000 characters.
2026-08-04 21:25:55 +02:00

133 lines
7.5 KiB
Markdown

# App Review notes
## The core problem, stated plainly
Punktfunk is the client half of a two-part system. Without a reachable host it shows a host list, a
pairing sheet, and settings — and nothing else. There is **no demo or offline mode in a release
build**: the mock-data screens in `Sources/PunktfunkClient/Screenshots/` are wrapped in `#if DEBUG`
and are compiled out of anything you ship. A reviewer who launches the App Store build with no host
on their network sees an empty "On this network" list.
Guideline 2.1 requires you to supply whatever is needed to fully exercise the app. So you must
attach **one** of:
- **(a) A reachable demo host.** Best outcome — the reviewer sees the real thing. Requires a host
exposed to the internet with its UDP ports forwarded, plus a pairing PIN in the notes. The client
can add a host by IP or hostname, so mDNS discovery is not required for this path.
- **(b) A demo video.** Apple accepts this for hardware- or setup-dependent apps. Less good: a
reviewer who cannot reproduce is a reviewer who can reject on something unrelated.
**Attach (a) if you can keep a host up for the review window; (b) is the fallback.** Whichever you
pick, fill in the placeholders before submitting — the template assumes (a) and marks the spots.
> **⚠ Decide before submitting:** if you go with (b), replace the "CONNECTING TO OUR DEMO HOST"
> section with the video URL and say explicitly that no host can be provided.
---
## Notes template — paste into App Store Connect
The App Review Information "Notes" field caps at **4000 characters**. The block below is **3919**,
and filling the five placeholders in shortens it further (the literal `[[FILL IN: …]]` text is
longer than the values that replace it). If you add to it, re-check the count — an over-long note
is silently truncated, and what gets cut is the end, where the privacy and entitlement answers
live.
```
WHAT THIS APP IS
Punktfunk is a low-latency game- and desktop-streaming client. It streams from a "host" the user
installs on their own gaming PC (Linux, or Windows 11 22H2+), over their own network. The host is
separate open-source software we publish at https://git.unom.io/unom/punktfunk; it is not sold,
and this app has no purchases.
This app is the client half only: it renders video and audio from the user's own machine and
sends input back. There is no content library and no server of ours in a session.
IMPORTANT: THIS APP NEEDS A HOST
With no reachable host, the app can only show its host list, the pairing screen and settings --
inherent to what it is, not an incomplete build. We have provided a live host for review.
CONNECTING TO OUR DEMO HOST
1. Launch Punktfunk. The main screen lists hosts on the local network. Ours is not on yours, so
add it by hand: "+" (top right) then "Add host"; on Apple TV, "Add host" on the main screen.
2. Enter: Host: [[FILL IN: hostname or IP]] Port: [[FILL IN: port, default 47998]]
Name it anything, then confirm.
3. The app connects and asks for a pairing PIN. Enter: [[FILL IN: PIN]]
A one-time SPAKE2 pairing; afterwards the device is remembered and needs no PIN.
4. The host's game library appears as a grid. Select any title to stream; video and audio start
within a few seconds.
5. While streaming: stats overlay = Ctrl+Alt+Shift+S (or three-finger tap on iOS/iPadOS); release
mouse = Cmd+Esc or Ctrl+Alt+Shift+Q; disconnect = Ctrl+Alt+Shift+D.
6. Settings (gear) covers decoder, bitrate, HDR, audio, controllers and profiles; the per-host
"Speed test" suggests a bitrate for the link.
The host stays reachable throughout review. If you cannot reach it, please contact
[[FILL IN: contact email]] and we will restore it promptly.
WHY THE APP ASKS FOR WHAT IT ASKS FOR
- Local Network: finds hosts via Bonjour (_punktfunk._udp) and connects to them -- the app's
entire purpose.
- Microphone (optional, off by default): audio goes to the user's own paired host, appearing
there as a virtual microphone for voice chat. Never recorded, never sent to us.
- networking.multicast: sends the Wake-on-LAN magic packet, which must go to a broadcast address:
a sleeping PC has no ARP entry, so unicast cannot reach it. Used for nothing else.
- device.usb / device.bluetooth (macOS): the GameController framework reaches wired controllers
through IOHIDLibUserClient and wireless ones through startWirelessControllerDiscovery. USB also
drives DualSense rumble, which CoreHaptics will not. Without these, no controller input.
- network.server (macOS): the app is outbound-only, but the App Sandbox gates bind() itself. Our
QUIC endpoint and UDP socket each bind a local port to receive host-to-client datagrams;
without this, no video, audio or rumble arrives.
- UIBackgroundModes "audio" (iPhone/iPad): a session carries real, audible audio from the host,
and this keeps it alive if the user steps away briefly. Backgrounded, video decoding stops, only
the real audio keeps rendering, and a bounded timer disconnects automatically. We never play
silence to stay alive, nor use the mode outside an audible session.
REGARDING BUILD 0.4.2 (3384)
That build was rejected under 2.4.5(i) for a temporary-exception entitlement
(mach-lookup.global-name, com.apple.audioanalyticsd), added on a mistaken belief about CoreHaptics
rumble under the App Sandbox. We have since verified rumble works without it; this build carries
no temporary exception.
ACCOUNTS, PURCHASES, DATA
No account, no sign-in, no in-app purchase. The app collects no personal data: no analytics,
tracking, advertising or crash-reporting SDKs, and no connection to any server of ours during a
session. Device identity is a keychain keypair used only to authenticate to the user's own host.
Privacy policy: [[FILL IN: https://punktfunk.unom.io/legal/privacy]]
```
---
## Before you submit — checklist
- [ ] Fill every `[[FILL IN: …]]` placeholder. There are five.
- [ ] Confirm the demo host is reachable **from outside your own network** — test it on cellular,
not on the LAN it lives on. This is the failure mode that wastes a review cycle.
- [ ] Confirm the pairing PIN in the notes is the one the host will actually accept during the
review window, and that pairing is left open (it is on-demand in the web console).
- [ ] Put at least one launchable title in the demo host's library. An empty grid after a
successful pairing looks like a broken app.
- [ ] If submitting tvOS, verify the whole flow is reachable with the **Siri Remote alone**. A
reviewer will not have a controller paired, and "requires an accessory to navigate" is a
tvOS rejection.
- [ ] Attach the demo video as a URL in the notes if you are going the (b) route.
## Separately worth checking: the privacy manifest
There is **no `PrivacyInfo.xcprivacy`** anywhere in `clients/apple`. The app does use
`UserDefaults` (`HostStore` reads the `group.io.unom.punktfunk` suite), and `UserDefaults` is one of
Apple's "required reason" APIs, which are expected to be declared in a privacy manifest. Apps
missing a declaration typically get an automated **ITMS-91053** notice on upload.
This is adjacent to the copy work rather than part of it, so nothing has been changed here — but it
is worth adding a manifest declaring `NSPrivacyAccessedAPICategoryUserDefaults` with reason code
`CA92.1` (access to an app group container) and `NSPrivacyTracking` set to `false`, before the next
submission. Confirm the current reason codes against Apple's documentation rather than taking the
code above on trust; the list has changed since it was introduced.