--- name: store-compliance description: >- Checks that rendered store assets will actually be accepted, and then ships them, using the open-screenshot-generator CLI: `osg verify` audits the produced PNGs and MP4s against App Store and Google Play rules (required size tiers, accepted alternatives, per locale caps, transparency, file size, aspect ratio, preview duration) and exits 3 on a failure, and `osg upload` pushes the set to App Store Connect or Google Play with the user's own developer credentials. Use this whenever someone asks whether their screenshots meet the store requirements, what sizes they need, why App Store Connect rejected an image, how many screenshots are allowed, or says things like "check these are valid", "upload to App Store Connect", "push to Play", "is this ready to submit" or "the store rejected my screenshots". license: MIT metadata: package: open-screenshot-generator homepage: https://openscrgen.app --- # Ship readiness: verify, then upload **The end state**: `osg verify` exits 0, `osg/osg.manifest.json` describes every file that exists, and either the set is uploaded to the right listing or the user has an exact list of what to fix. Run `verify` before every upload, without exception. Both stores accept an upload and then fail it asynchronously, App Store Connect hours later during asset processing, and a failure at that point gives you a state code rather than an explanation. ## The exit codes are the contract | Code | Meaning | What to do | | --- | --- | --- | | 0 | everything passed | ship it | | 1 | usage, config or environment | fix the config or the machine, nothing was produced | | 2 | the browser, the page or a tool call failed | a render problem, re-run with `--verbose` | | 3 | the files exist and a store rule rejects them | fix the named rule and re-render | ```bash npx -y open-screenshot-generator@0 verify npx -y open-screenshot-generator@0 verify --json ``` `--json` prints one machine readable object with a row per finding, which is what to use in CI and what to parse when reporting back. ## What verify checks, and what each failure means | Finding | What it means | Fix | | --- | --- | --- | | Missing tier | a format in `formats` produced no file | re-run `render`, or drop the tier from the config | | Wrong pixel size | the board size is not an accepted size for that tier | set the artboard to the preset, or change `formats` | | Too many images | more than the store's cap for that slot and locale | drop boards, the first two are what people see anyway | | Too few images | under the store's floor (Play needs 2 phone shots) | add a board | | Transparency | the PNG carries an alpha channel | give the board an opaque background, then re-render | | File too large | over the store's per image limit | reduce visual complexity, or render the smaller accepted size | | Aspect ratio | Play's long side is more than twice the short side | render the Play tier, not the iPhone tier, see below | | Locale gap | one language has fewer boards than the base | the missing boards did not render, check the locale list | | Placeholder copy | a headline still says the template's text, or a board is still named "Blank Artboard" | rename the board and write the copy, the board name becomes the file name | | Empty device frame | a device slot still holds shipped placeholder art | place the app's real screenshot in it | | Video duration | outside 15 to 30 seconds | `set_preview_duration`, or `video.duration` | | Video dimensions | not 886x1920 or 1920x886 | re-render, the size is forced by the mode | | Video codec | not H.264 | the browser has no H.264, see **app-preview-video** | Placeholder copy and an empty device frame are not store rules. They are the two failures that actually get shipped, so `verify` treats them as failures too. ## App Store: required tiers and accepted alternatives You do not need every size. Apple scales the required tier down to the smaller ones. | Slot | Default size | Also accepted | Status | | --- | --- | --- | --- | | iPhone with Dynamic Island (medium display) | 1206x2622 | 1179x2556, or the rotated sizes | required for every iPhone app | | iPhone with Dynamic Island (large display) | 1290x2796 | 1260x2736, 1320x2868, or the rotated sizes | optional separate slot | | iPad 13 inch | 2064x2752 | 2048x2732 | required if the app runs on iPad | | Apple Watch | 422x514 | 410x502, 416x496, 396x484, 368x448, 312x390 | required for a watch app | | Mac | 2560x1600 | 2880x1800, 1440x900, 1280x800, always 16:10 | required for a Mac app | | Apple TV | 1920x1080 | 3840x2160 | required for a tvOS app | | Apple Vision Pro | 3840x2160 | | required for a visionOS app | Older iPhone and iPad slots (6.5 inch, 5.5 inch, 4.7 inch, iPad 11 inch, 10.5 inch, 9.7 inch) still exist and are optional. Fill them only if the user asks. Every iPhone and iPad slot also accepts the rotated size, so a landscape app renders the same tier sideways. Watch, Mac, TV and Vision are one orientation only. The default CLI format is `ios-6-3` (1206x2622), and `ios` and `iphone` mean the same. `ios-6-9` and `ios-large` convert to the optional large slot at 1290x2796. The bundled templates are built at 1206x2622, so a project built from one already fits the required slot. Other canvas presets require creating or resizing the artboards first, then rendering or uploading with `--formats as-is` to keep their dimensions. Caps and formats: - **10 screenshots** maximum per display type per localization, and at least one is required. - **3 App Previews** maximum per localization per device family. - PNG or JPEG, RGB, flattened. No transparency. - The pixel size has to be exactly one of the sizes that display type accepts. Nothing is resized for you, and a size that is close is not accepted. ## Google Play: slots and limits | Slot | Size rule | Count | | --- | --- | --- | | Phone screenshots | 320 to 3840 px per side, long side at most 2x the short side | 2 to 8, at least 2 required | | 7 inch tablet | same rule, 1200x1920 recommended | up to 8 | | 10 inch tablet | same rule, 1600x2560 recommended | up to 8 | | Wear OS, Android TV | same rule | up to 8 each | | Feature graphic | exactly 1024x500 | 1, required on every listing | | App icon | exactly 512x512 | 1, required to publish | | Android TV banner | exactly 1280x720 | 1 | PNG or JPEG, 8 MB maximum per image. **Play's aspect rule blocks the iPhone canvas.** 1206x2622 is 2.17:1, and Play needs the long side to be at most twice the short side. Play rejects it. Use the `play-phone` preset (1080x1920) for the Play tier rather than re-uploading the App Store files. That rule is Google's, verified in their own docs, and it is not negotiable. Play also wants a promo video as a **YouTube URL**, not an uploaded file. The `styled` video mode produces a good file for that, but you upload it to YouTube and paste the link into Play Console. ## Uploading ```bash npx -y open-screenshot-generator@0 verify && npx -y open-screenshot-generator@0 upload ``` The upload uses the user's own developer credentials and talks to Apple and Google directly. No server belonging to this project is involved at any point. ### Credentials, handled safely Rules, in priority order: 1. **Never put a key in `osg/osg.config.ts`.** That file is committed. There is no credential field in it, on purpose. 2. **Never paste a key into a command line.** It lands in shell history and in your transcript. 3. **Point at a file, and gitignore the file.** Apple's key is a `.p8`, Google's is a service account JSON. Add both to `.gitignore` before you download them. 4. **Never echo, cat or print a key file**, and never read one into your own context to "check it". You do not need to see it to use it. 5. If the user pastes a key into the conversation anyway, tell them to rotate it. Set the environment variables the CLI documents, then run the command. Confirm the exact names with `osg upload --help` for the release in use: ```bash export OSG_ASC_ISSUER_ID=... # App Store Connect API issuer id, a UUID export OSG_ASC_KEY_ID=... # the key id export OSG_ASC_KEY_FILE=./secrets/AuthKey_XXXX.p8 npx -y open-screenshot-generator@0 upload ``` ```powershell $env:OSG_ASC_ISSUER_ID = '...' $env:OSG_ASC_KEY_ID = '...' $env:OSG_ASC_KEY_FILE = './secrets/AuthKey_XXXX.p8' npx -y open-screenshot-generator@0 upload ``` PowerShell has no inline environment prefix, so `VAR=x command` does not work there. Set them on their own lines, as above. For Google Play, one variable pointing at the service account JSON: ```bash export OSG_PLAY_KEY_FILE=./secrets/play-service-account.json npx -y open-screenshot-generator@0 upload --store play ``` The Apple key needs the **App Manager** role. The Play service account has to be invited in Play Console **and** the Android Developer API has to be enabled on the project. ### What each store actually does with the upload **App Store Connect** reserves each asset, uploads it in chunks to pre signed URLs, then commits it with a checksum. Every one of those calls returns 2xx for a wrong sized image. Apple then processes the asset asynchronously and it fails minutes to hours later. The upload therefore polls the delivery state and reports what Apple actually concluded, which is the only reason it can be trusted when it says the upload worked. Do not treat a fast success message from any other tool as acceptance. Screenshots hang off a set scoped to (version, localization, display type), so a mixed project is grouped by resolved display type and each group gets its own set. **Screenshots are frozen once a version is submitted.** Only a version in an editable state accepts them, and writing to one in review comes back 409. The version has to leave review first. Nothing is preselected for you, because arming an upload against a frozen version is worse than asking. **Google Play** opens a staged edit, uploads into `listings//`, validates, then commits. Nothing is public until the commit, and a thrown error discards the whole edit, so a failed run cannot leave a half updated listing. Some accounts refuse automatic review submission and say so in the commit error; the client retries without sending for review and reports that it did. ## Troubleshooting **Apple returns 401 or 403.** Either the key is wrong or the key does not have the App Manager role. The two are indistinguishable in practice, so check both rather than guessing. **Play returns 403.** Almost always setup, not code: the service account was never invited in Play Console, or the Android Developer API is not enabled. **Play returns 404.** A package name typo, or an app that has never been published. Play refuses API edits before the first release. **Apple accepted it and then the screenshot disappeared.** The asset delivery state went to FAILED. That is a size or a format problem, and `osg verify` catches every case of it before the upload. **"This version cannot be edited."** It is in review or already released. Create the next version in App Store Connect first. **Verify passes and the store still complains about content.** The mechanical rules are what `verify` covers. Review guidelines are not mechanical: do not show a device frame inside the screenshot of a different device, do not show competing platforms' branding, do not put an award or a rating in the image that the app did not earn, and do not show UI that is not in the shipped build. Those are human review calls and they are also the ones that cost a week. **A screenshot shows content that is not in the build.** That is the most common human review rejection of all. If the design invented a stat, a chart or a testimonial, change it. ## Related skills - **store-screenshots** for producing the set in the first place - **app-preview-video** for the video rules, which are stricter than the image rules - **store-localization** for per locale caps and folders - **editor-tools** for fixing what `verify` flagged, one element at a time