--- name: store-pitfalls description: Known pitfalls with App Store Connect, Google Play and storebound itself, learned from real releases, and what to do about each - store lists that lag right after a create, dry runs that print a count instead of a plan, App Store names taken on a storefront, keyword limits in bytes, Data safety answers without code evidence, missing font glyphs in screenshots, console sign-in and sessions, Capacitor monorepo plugins, duplicates from retries, re-runs and parallel runs, App Store search ranks that are only a trend, Play without a search API, Play's Create app form (no API, opener and submit share a name, Free is final), App Privacy purposes over-mapped (Play advertising is Third-Party Advertising only), an unknown console page (bounded wait and a debug dump), a signed-in console read as a sign-in page (Google account widget frames), console forms driven agent-first, stale Capacitor web assets. Read it before any store task, and when a store command does something you did not expect. --- # Store pitfalls Each entry: **symptom** → cause → what to do → what guards it now. The full changelog of lessons with dates is `docs/pitfalls.md` in the storebound repository. When you hit a new one, tell the owner and add it there (and here). ## 1. A product you just created is "not there" - **Symptom:** `apple iap-review` uploads nothing for a product `apple iap` created a minute ago; a list looks short. - **Cause:** App Store Connect's `/inAppPurchasesV2` list (and Play's product list) can lag for a short while after a create. A command that walks the list skips what is not in it yet. - **Do:** wait a minute and re-run. Never assume a product was handled because the command finished. - **Guard:** `apple iap-review` compares the config's products with the store's list, names the missing ones ("the store's list can lag a minute after creating; re-run") and exits 1. `apple iap` and `play products` read the list again after creating and warn about any new product that is not in it yet; their dry runs name products that are not on the store. ## 2. A dry run that only prints a count - **Symptom:** `--dry` says "listing: 20 locales" or "3 change(s)", but not which fields would change to what. - **Cause:** the command logged a summary instead of the plan. - **Do:** never ask the owner to approve a count. Show the rows that change (item, action, old → new). If a command only prints a count, say so and show the owner the diff from `pull` / `--json` output instead. - **Guard:** `cli/lib/dry.ts` `dryPlan(title, rows)` prints the standard plan table (`apple iap`, `apple iap-review`, `play products` use it); `tests/pitfalls.test.ts` scans `cli/**` for `--dry` branches that print nothing but a count. ## 3. "The app name you entered is already being used" - **Symptom:** creating the App Store record or saving the name fails although you own no app with that name. - **Cause:** App Store app names are unique per storefront, across all developers. - **Do:** ask the owner for a variant (a short suffix such as "Example Notes: Lists") and set `apple.name` in the config; keep `app.name` for Play and the brand. Do not pick the suffix yourself. - **Guard:** `apple.name` overrides the App Store name. ## 4. Keyword field "too long" at fewer than 100 characters - **Symptom:** App Store Connect rejects keywords that look short enough, mostly in Turkish, German, CJK. - **Cause:** the 100 limit counts UTF-8 bytes, not characters ("ş" is 2 bytes, a CJK character 3). - **Do:** shorten by bytes; drop words, not commas. - **Guard:** `storebound check` counts bytes for keywords. ## 5. Data safety says "email collected" for an app that never asks for one - **Symptom:** the proposal marks email (or phone) as collected because the app has accounts. - **Cause:** account features do not imply email collection (anonymous ids, Game Center, Play Games sign-in). - **Do:** answer only what the code proves; ask the owner for the rest (rule 5). - **Guard:** `storebound scan` marks email only with evidence (an email field, an email sign-up call) and otherwise says "ask owner". ## 6. Boxes or wrong glyphs in localized screenshots - **Symptom:** Turkish, Polish or Vietnamese captions show fallback glyphs (ş, ğ, ł, ệ) while the English ones are fine. - **Cause:** the font (or the Google Fonts subset that was downloaded) does not cover those characters, and the browser silently draws them with another font. - **Do:** check every language's characters before rendering, pick a font that covers them or let the fallback fill in. - **Guard:** `storebound shots fonts` / `shots check` check each language's characters against the fonts' glyphs. ## 7. Console automation stops at sign-in or loses the session - **Symptom:** `console fill` waits on a sign-in page, cannot find fields, or is signed out on the next run. - **Cause:** the App Store Connect sign-in runs inside an `idmsa.apple.com` iframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies unless the last session is restored. - **Do:** let the owner sign in in the opened window (never type credentials); re-run after the sign-in. - **Guard:** the console session handles the iframe and the plain heading and starts Chrome with `--restore-last-session`; a partial miss fills every field found and leaves only the missing ones to the owner. ## 8. `scan` misses Capacitor plugins in a monorepo - **Symptom:** SDKs or privacy manifests of Capacitor plugins are not found. - **Cause:** the plugins live in `apps/native/node_modules` (a workspace package), not in the root `node_modules`. - **Do:** run `scan` from the project root; check that the stack line lists the plugins. - **Guard:** `storebound scan` and `privacy-manifest` resolve packages through workspace folders too. ## 9. A second run (or a retry) creates a duplicate - **Symptom:** two in-app purchases or subscription groups with the same name, a screenshot twice in a set, an offer code campaign or a custom product page twice, after a timeout, a crash, a re-run a minute later, or two runs at once. - **Cause:** a POST create retried after a timeout or a 5xx may have been applied the first time; the store's lists lag right after a create, so a re-run does not see what the last run made; two writers race; a Play edit redone after a commit whose answer was lost. - **Do:** just run the command again: every write command is an upsert and converges (`docs/idempotency.md`). Never retry a create by hand (`storebound api`) without a GET first. If a run says another storebound run holds the lock, wait for it; use `--force-unlock` only when that run is really gone. Never run two write commands for the same app in parallel. - **Guard:** `cli/lib/upsert.ts` (findOrCreate: fresh re-check + journal `.storebound/created.json`; createOnce: 409 = found, an unclear answer is looked up, never re-sent blindly), retries only for requests that are safe twice, Play edits deleted on every error and never redone after an unclear commit, screenshot / preview sets keep the matching prefix in order, `.storebound/locks/` per app and store. `tests/idempotency.test.ts`. ## 10. "We rank #4 for this keyword" (from the Search API) - **Symptom:** `aso rank` says #4, the owner's phone shows the app lower (or higher) in App Store search. - **Cause:** the iTunes Search API order is close to, not equal to, App Store search: no personalisation, no search ads, no in-app event rows. It has no subtitle field either. - **Do:** read ranks as a trend over weeks (`aso audit` arrows), never as a promise; never claim a competitor's subtitle (no public API has it); a popularity number exists only from Apple Ads, never estimate one. - **Guard:** `aso rank`, `aso competitors` and `aso audit` say it in their output; `aso suggest` shows popularity "unknown" without Apple Ads credentials. ## 11. "Check our Google Play keyword ranks" - **Symptom:** the owner asks for Play ranks or Play competitors per keyword. - **Cause:** Google Play has no public search API; reading Play search pages would be scraping. - **Do:** say so. Measure Play through the Play Console acquisition reports (search terms) and `analytics snapshot` (store listing visitors, acquisitions); improve the listing text with `aso audit` (short description, density). - **Guard:** `aso` covers App Store search only; the aso-basics skill states the limit. ## 12. A new Play app: "Create app" is both the opener and the submit - **Symptom:** an agent cannot create the Play app through the API, or clicks "Create app" without knowing whether it opens the form or submits it; or an app goes out as Free and the owner later wants it paid. - **Cause:** the Play Developer API cannot create apps. Play Console's Home has a "Create app" button that opens the form, and the form's final button has the same words. Free cannot become Paid later on Google Play. The form (`/console/u/0/developers//create-new-app`, verified 2026-10-12) has an App package name field (max 150) with a read-only "Check availability" button ("Package name available"); the package name is fixed for good. App or game and Free or paid are ``s: setting the native input leaves the form saying "Select app or game" until the inner `[role=radio]` is clicked (a JS click works in a background window). - **Do:** `storebound console plan play-app-record` and drive the form with your browser tool (or the scripted `console fill play-app-record`); fill the package name and press Check availability; click the inner `[role=radio]`s and check that no "Select …" hint is left (`console verify play-app-record`); tell the owner that Free is final; the owner ticks the declarations and presses Create app. Afterwards the app lives at `/console/u/0/developers//app//app-dashboard` (set `play.appId`). The service account's app-level permissions do NOT cover the new app: the OWNER adds it (Users and permissions > the service account > Add app > Admin, or the minimal set > Apply > Save changes; never an agent: it is a permission grant), then `storebound apps check --store play`. - **Guard:** `createAppAllowed` lets the opener through only on the app list, never inside a form or dialog (English and Turkish); agreement checkboxes are never ticked; Free or paid is outlined as an owner check. ## 13. App Privacy purposes over-mapped - **Symptom:** App Privacy says Developer's Advertising or Marketing for Device ID, Coarse Location, Product Interaction or Performance Data, although the app only shows an ad SDK's ads. - **Cause:** a Play purpose was turned into two Apple purposes ("advertising" -> Third-Party Advertising AND Developer's Advertising or Marketing). Apple's definitions (developer.apple.com/app-store/app-privacy-details/): third-party ads are Third-Party Advertising; Developer's Advertising is first-party ads or marketing to your users. - **Do:** one Apple purpose per Play purpose; put the owner's real answers in `appPrivacy.types` (Apple type ids, purposes, linked, tracking) and let `storebound check` compare them with Info.plist and PrivacyInfo.xcprivacy. - **Guard:** `applePurpose` (cli/lib/declarations.ts), `appPrivacy.types` validation and `appPrivacyMismatches` (cli/lib/app-privacy.ts), `tests/app-privacy.test.ts`. ## 14. An unknown console page: bounded wait and a debug dump - **Symptom:** `console fill` clicked Next (or opened the form) and then nothing happened in the log for a long time. - **Cause:** the console showed a page storebound does not recognise (a changed layout, a new question): no field it can fill and no button it may click. - **Do:** read the warning: it names the page ("the page after "Next""), its buttons, and where the debug dump is. Do that part by hand (the manual steps follow), or stop. To report it, re-run with `--debug-dump` and attach the folder `.storebound/console-debug/
-