ProtoViewer store listing as code¶
Issue #14152 under #14142. The Play and App Store listing text, the app icon and the screenshot plan live in the repository, so a listing change is reviewable and diffable and a stale or over-length listing fails Fast Validation instead of failing at upload.
Layout¶
fastlane/metadata/
android/<locale>/{title,short_description,full_description,video}.txt
android/<locale>/changelogs/{default,<versionCode>}.txt
android/<locale>/images/{phone,sevenInch,tenInch}Screenshots/ (generated)
ios/<locale>/{name,subtitle,description,keywords,promotional_text,release_notes}.txt
ios/<locale>/{privacy_url,support_url,marketing_url}.txt
ios/<locale>/screenshots/ (generated)
ios/review_information/notes.txt
store-listing/icon/{background,foreground}.svg
scripts/store-listing/
The tree follows the fastlane supply (Play) and deliver (App Store) conventions, which F-Droid
also reads, so a future upload lane needs no conversion. en-US is required; any other locale
directory is validated by the same rules. Neither store has a repository-hosted source of truth
today: the stores keep whatever was last uploaded, and this tree is what gets uploaded.
The text is provisional until the owner decisions in #14143 (policy positioning, review notes) and
14144 (store name and URLs) land. The privacy URL points at a page that does not exist yet.¶
Validation¶
bun scripts/store-listing/validate-store-listing.ts runs in Fast Validation as the store-listing
check. It reads files only. Lengths count Unicode code points, as the consoles do.
| Field | Limit |
|---|---|
| Play title | 30 |
| Play short description | 80 |
| Play full description | 4000 |
| Play changelog (per release) | 500 |
| App Store name | 30 |
| App Store subtitle | 30 |
| App Store keywords (comma separated) | 100 |
| App Store promotional text | 170 |
| App Store description, release notes | 4000 |
App Review notes (review_information) |
4000 |
| Privacy, support and marketing URLs | https:// |
It also rejects missing required files, empty or whitespace-padded files, CRLF line endings,
multi-line titles, keywords with spaces around commas, duplicate keywords, a keyword that repeats the
app name, a subtitle equal to the name, unknown files in a locale directory, and a locale directory
that is not a store locale code. The two icon layers must be well-formed SVG on the shared
0 0 108 108 canvas. The rules and limits live in scripts/store-listing/storeListing.ts.
Icons¶
The single vector source is a layer pair on the Android adaptive-icon canvas: background.svg and
foreground.svg, both viewBox="0 0 108 108". Keep every foreground shape inside the central
66 x 66 safe zone (coordinates 21 to 87), and draw detail as holes (fill-rule="evenodd") so the
themed monochrome layer keeps it.
bun scripts/store-listing/generate-icons.ts [--out <dir>] # default build/store-listing/icons
bun scripts/store-listing/generate-icons.ts --list # print the target table
The generator renders with sharp, an existing direct dependency (libvips draws the SVG), so there
is no new package. The table in scripts/store-listing/iconTargets.ts produces:
- Android, per density (mdpi to xxxhdpi): adaptive
foreground,backgroundandmonochromelayers (108 dp) and the rounded legacyic_launcher(48 dp). - Play: the 512 px store icon.
- iOS: an
AppIcon.appiconsetwith the 1024 px icon in light, dark and tinted appearances (opaque, no alpha) and itsContents.json.
Two runs over the same sources write byte-identical files. The PNGs are build output and are
git-ignored: this repository routes *.png through Git LFS, and generated art does not belong in
history. Distinct dev and beta icons (as in #14152’s proposal) are a second layer pair plus a channel
column in the table; they are not drawn yet.
Screenshot plan¶
scripts/store-listing/screenshotPlan.ts is the plan as data: four screens (library, viewer, import,
share), light and dark, on pinned devices (Android phone, 7 inch and 10 inch AVDs; one iPhone
6.9 inch simulator; the iOS viewer is iPhone-only), 32 shots in all. Each shot acquires its pinned
device, sets appearance, fixes the clock and bars with the statusBar tool (9:41, full battery),
clears the app, seeds a fixed sample library from the shared valid prototype corpus, drives the
screen with tapOn and launchApp, and captures with observe (screenshot: "settled").
bun scripts/store-listing/capture-screenshots.ts --list
bun scripts/store-listing/capture-screenshots.ts --emit <dir> --android-serial <serial> --ios-udid <udid>
--emit writes one <shot>.plan.json for bun scripts/mcp-drive.ts --private --plan <file> and a
manifest.json with each shot’s host steps (night mode through adb or simctl ui), expected
pixel size and destination under fastlane/metadata. Use a dedicated capture emulator and simulator,
never a shared development device. Element labels come from the app’s string resources.