README is a short front page (about 120 lines) with one real visual per section; every former section moved unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth, studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools: tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
102 lines
12 KiB
Markdown
102 lines
12 KiB
Markdown
# Catch rejections before you submit
|
|
|
|
App Store rejections cost days: you wait for review, read one sentence in the Resolution Center, fix it, and wait
|
|
again. On Google Play a policy problem can mean a blocked update, a removal or a strike on the account. Most of the
|
|
usual ones are visible in the project long before that. `storebound scan` reads the project itself, offline and
|
|
read-only (Xcode project, Info.plist and build settings, entitlements, PrivacyInfo.xcprivacy, Podfile.lock,
|
|
Package.resolved, Cartfile, Gradle files and version catalogs, AndroidManifest.xml (and the merged manifest of a local
|
|
build), res/xml and strings.xml, CMake / ndk-build files, Unity ProjectVersion.txt, package.json,
|
|
Expo app.json, capacitor.config, Cordova config.xml, pubspec, Unity Packages/manifest.json, ProjectSettings and
|
|
External Dependency Manager files, Godot export presets), plus your config and listing files. It works out the stack
|
|
and the SDKs (ads, analytics, attribution, crash, sign-in, payments, push, with versions and the file:line they come
|
|
from) and reports every rejection risk it can prove, each with the guideline, the evidence, the fix, a severity and the
|
|
skill to open. On the teaser fixture (`tests/fixtures/scan/teaser`):
|
|
|
|
```text
|
|
$ storebound scan
|
|
Rejection risks: 2 blocker, 3 high, 0 medium, 0 low
|
|
1. BLOCKER 2.1(a) · App Store · Sign-in is required but there is no demo account for App Review
|
|
at storebound.config.ts:12 reviewAccess: { signInRequired: true, instructions: 'Sign in with the demo account.' },
|
|
at storebound.config.ts:11 accounts: { enabled: true },
|
|
at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0
|
|
2. BLOCKER 5.1.1(v) · App Store · Accounts can be created but no in-app account deletion was found
|
|
at storebound.config.ts:11 accounts: { enabled: true },
|
|
at Planner/LoginView.swift:11 Button("Create account") { Task { try await AccountApi.signUp() } }
|
|
at Planner/LoginView.swift:17 static func signUp() async throws {
|
|
3. HIGH 4.8 · App Store · Third-party login (Google / Facebook / ...) without Sign in with Apple or another equivalent login option
|
|
at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0
|
|
at Planner/LoginView.swift:9 GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController()) { _, _ in }
|
|
4. HIGH 3.1.1 · App Store · Restorable purchases (non-consumables or subscriptions) but no Restore Purchases path was found
|
|
at storebound.config.ts:16 com.example.planner.pro (non_consumable)
|
|
5. HIGH 5.1.2(i) · App Store · Tracking-capable code without App Tracking Transparency (Google AdMob, IDFA)
|
|
at Podfile.lock:2 Google AdMob: Google-Mobile-Ads-SDK 11.10.0
|
|
at Planner/Ads.swift:7 print(ASIdentifierManager.shared().advertisingIdentifier)
|
|
```
|
|
|
|
Every finding also has a `fix:` line and the skill. Below the risks comes the declarations proposal: the Play Data
|
|
safety and App Store App Privacy answers, App Tracking Transparency and privacy manifest entries the detected SDKs and
|
|
code imply, each marked **detected (file:line)** or **ask owner**, as a diff against `dataSafety` / `appPrivacy` in
|
|
the config. Nothing is written unless you run `storebound scan --write` (only detected answers; questions stay
|
|
questions). The MCP tool `scan` is read-only and cannot write; `validate` includes the open scan findings; the desktop
|
|
app shows them on the Checks tab. A finding you decided is fine goes into the config with its reason:
|
|
`scan: { ignore: [{ rule: 'demo-account', reason: '...' }] }`; a rule with several checks (e.g. `play-metadata`) takes `match: '<text in the finding title>'` to accept just one of them.
|
|
|
|
| rule | App Store | Google Play | what scan proves |
|
|
|---|---|---|---|
|
|
| `demo-account`, `play-app-access` | 2.1(a) | App access | a sign-in (config, auth SDKs, sign-in code) without `reviewAccess` or without the demo account in `.storebound/` |
|
|
| `account-deletion`, `play-deletion-url` | 5.1.1(v) | User Data: account deletion | sign-up or social login without a deletion path in code or `accounts.inApp`; no `accounts.deletionUrl` |
|
|
| `login-services` | 4.8 | | Google / Facebook / other third-party login without Sign in with Apple or another equivalent option |
|
|
| `restore-purchases`, `external-purchase`, `play-payments` | 3.1.1 | Payments | non-consumables or subscriptions without a restore path; card / PayPal SDKs next to in-app products; "buy on our website" (Play: unless `play.billingPrograms` lists user choice billing or external offers) |
|
|
| `subscription-info` | 3.1.2(c), 3.1.2 | | no Terms of Use URL, no Terms link or renewal wording in the description, no legal links or "Manage subscription" in the app |
|
|
| `att` | 5.1.2(i) | | tracking-capable SDKs or IDFA without ATT; the prompt without its purpose string; the string without a prompt |
|
|
| `purpose-strings`, `unused-permission` | 5.1.1(ii), 5.1.1(iv), ITMS-90683 | | a used permission without its NS*UsageDescription; empty or vague strings; declared but unused |
|
|
| `privacy-manifest` | ITMS-91053 / 91061 | | no PrivacyInfo.xcprivacy, required-reason APIs not declared, SDKs older than their first manifest version |
|
|
| `privacy-policy-link`, `play-privacy-policy` | 5.1.1(i) | User Data: privacy policy | no privacy link in the app's sources (Play: every app; medium when it collects data), an `http://` link |
|
|
| `privacy-labels` | 5.1.2 | User Data: Data safety | SDK data types missing from `dataSafety`; Play also: an answer narrower than the SDK's disclosure (purposes, sharing), SDKs with no `dataSafety` at all |
|
|
| `play-prominent-disclosure` | | User Data: prominent disclosure, Location permissions | background location without a disclosure that says "location" and "background"; location, contacts, calendar or phone number access without any disclosure or rationale text in sources or `strings.xml` |
|
|
| `play-background-location`, `play-photo-video`, `play-all-files-access`, `play-accessibility`, `play-install-packages`, `play-health-connect`, `play-full-screen-intent`, `play-vpn` | | Location permissions, Photo and Video Permissions, All files access, AccessibilityService API, REQUEST_INSTALL_PACKAGES, Health Connect, Full-screen intent, VpnService | the restricted permission or service in the manifest (or the merged manifest of a local build), each with the core-use test and the Play Console declaration it needs; `isAccessibilityTool` lowers the accessibility finding to medium |
|
|
| `play-fgs`, `play-exact-alarm`, `play-query-all-packages`, `play-sms-call-log` | | Foreground service types, exact alarms, package visibility, SMS and Call Log | foreground services without a type or its permission (and the Console declaration); USE_EXACT_ALARM; QUERY_ALL_PACKAGES; SMS and call log permissions |
|
|
| `kids-ads`, `play-families-ads`, `play-families-ad-id`, `play-families-location` | 1.3 | Families | a Kids Category age band with third-party ads or analytics SDKs; `play.targetAges` under 13 with an ad SDK that is not Families self-certified (or older than its certified version), the AD_ID permission in a child-only app (mixed audience: ad / attribution SDKs need child-directed treatment), location in a child-only app |
|
|
| `play-ad-id` | | Advertising ID | targetSdk 33+ with ad / attribution SDKs and no AD_ID permission (medium when the merged manifest proves it is missing; Google Mobile Ads adds it itself) |
|
|
| `play-16kb` | | 16 KB page sizes (updates blocked from 2027-02-01) | Unity before 6000.0.38f1 / 2022.3.56f1 / 2021.3.48f1, React Native before 0.77, NDK before r28 for the app's own C/C++ without the 16 KB flags, AGP before 8.5.1 with uncompressed native libraries; else prebuilt `.so` files to check |
|
|
| `play-ads-launch` | | Ads, Better Ads Experiences | an interstitial shown from `onCreate` of the launcher activity (Android native; other engines: review-only) |
|
|
| `play-testing-requirement` | | Testing for new personal accounts | `play.accountType: 'personal'` created after 2023-11-13 (12 testers, 14 days of closed testing before production); low when the account type is not set |
|
|
| `other-platforms` | 2.3.10 | | "Android", "Google Play" and other stores in the App Store text or screenshot captions |
|
|
| `metadata-keywords`, `kids-terms`, `play-metadata` | 2.3.7, 2.3.8 | Metadata | other apps' names, prices and "best / #1" in name, subtitle or keywords; keyword lists; "For Kids" outside the Kids Category. Play title over 30 characters, emoji, ALL CAPS, rank / price / promotion terms; other apps in the title or short description; repeated keywords or keyword lists in the full description |
|
|
| `play-impersonation` | | Impersonation | a well-known brand in the package name or app name (low: a check, not a verdict) |
|
|
| `placeholder-text` | 2.1(a) | Metadata | lorem ipsum, TODO, template lines in the listings |
|
|
| `beta-wording` | 2.2 | | "beta", "demo", "trial version", "test build", "early access" in the app name (config, CFBundleDisplayName) or the App Store name, subtitle, keywords, promotional text and description ("demo account" is fine) |
|
|
| `originality` (with `--originality`) | 4.3 | Repetitive content | a near-duplicate of one of your own apps (`storebound originality` scores: name, listing text, icon and screenshot hashes, project structure) |
|
|
| `min-functionality` | 4.2 | | Capacitor `server.url` / Cordova `<content src>` on a remote site; a few native files around a remote WKWebView |
|
|
| `deprecated-api`, `private-api` | 2.5.1 (ITMS-90809) | | UIWebView; `App-Prefs:` / `prefs:root` URL schemes |
|
|
| `ipv6`, `ats` | 2.5.5, App Transport Security | | IPv4 literals in server URLs; NSAllowsArbitraryLoads |
|
|
| `export-compliance` | export compliance | | no ITSAppUsesNonExemptEncryption and no `apple.usesNonExemptEncryption`, or the two disagree (never guessed) |
|
|
| `review-prompt` | 5.6.1, 3.2.2(x) / 5.6.3 | | a custom "rate us" prompt without the system API; rewards for ratings |
|
|
| `play-target-sdk` | | Target API level | below API 36 (Wear OS / Automotive 35, TV / XR 34) for new apps and updates since 2026-08-31 |
|
|
| `play-package-name` | | Play Console upload | the reserved `com.example` prefix |
|
|
|
|
`storebound scan --originality [--against <folders>]` adds the Guideline 4.3 / Play repetitive content check against
|
|
your other apps to the same report (folders default to `originality.against` in the config; `storebound originality`
|
|
stays the command with every score and suggestion).
|
|
|
|
Rules cite Apple's App Review Guidelines and the Play Policy Center / Play Console Help pages they come from (checked
|
|
2026-10-11, in the code comments of `cli/lib/scan/rules.ts` and `cli/lib/scan/play-rules.ts`); the SDK table with each
|
|
vendor's data disclosure page and Google's Families self-certified ads SDK list is `cli/lib/scan/sdks.ts`. Play config
|
|
the rules read: `play.targetAges` (Target audience age groups), `play.accountType` / `play.accountCreated`, and
|
|
`play.billingPrograms`. When the project was built locally, scan also reads the merged manifest under
|
|
`build/intermediates` (the permissions libraries add); otherwise it says that it could not. Not a rule because no Play
|
|
policy page covers it: device admin apps (BIND_DEVICE_ADMIN); not a policy at all: Play Integrity / anti-tamper.
|
|
|
|
**Checked at review, not by scan** (no file shows them): crashes and hangs on the reviewer's device (2.1, `store-health`),
|
|
links that are dead at review time (`validate` checks the configured URLs), screenshots and descriptions that do not
|
|
match the app (2.3, `store-screenshots`), unfinished UI (4.0), "too simple" and spam / template apps (4.2, 4.3,
|
|
`portfolio-originality`), IAPs that fail in the sandbox (2.1(b), `store-iap-setup`), the content of the privacy policy
|
|
(`privacy-policy`), local law (5.2). On Google Play: whether a restricted permission really serves the app's core
|
|
purpose (the Console declaration and its video decide), the wording and timing of a prominent disclosure, ad
|
|
placements in Unity, Flutter or React Native code, the Health apps declaration answers, and the alignment inside a
|
|
prebuilt `.so` (scan lists them; `zipalign -c -P 16` and `check_elf_alignment.sh` check the built app). The
|
|
`review-rejections` skill covers what to do when one of these comes back.
|
|
|
|
---
|
|
|
|
[All docs](README.md) · [README](../README.md)
|