README, docs and site: one story (the whole job, no avoidable rejections, the desktop app, your AI, install)

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.
This commit is contained in:
CRTkafa 2026-10-13 03:29:59 +08:00
parent deb218943c
commit 710c3a53d0
63 changed files with 2742 additions and 1782 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

15
site/img/term/check.txt Normal file
View file

@ -0,0 +1,15 @@
$ storebound check
[check] config ok: storebound.config.ts
[check] play listing: 2 languages
[check] hint: the App Store name is "Example App" in every locale: App Store names are unique, so if it is taken App Store Connect refuses the record ("already being used"). Then set apple.name with a short suffix, e.g. "Example App: <tagline>" (30 characters at most); app.name stays the Play and on-device name
[check] appstore listing: 2 locales
[check] data safety: 768 CSV rows
[check] shots copy: 2 languages x 4 scenes
[check] hint: Play App category "Productivity" (apple.info.categories (PRODUCTIVITY)) is a console step, not an API one: `storebound console plan play-store-settings`
[check] WARN 31 Play App content answer(s) are not in the config: ask the owner in one batch at the start (`storebound console plan play-app-content` lists them), then write them into the config
- App Review needs a demo account (sign-in required) · add reviewDemoAccount to .storebound/credentials.json · review-access skill
- missing file: store/icon/icon.png
- missing file: store/icon/foreground.png
- missing file: store/icon/background.png
error: 4 problems

BIN
site/img/term/check.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View file

@ -0,0 +1,160 @@
$ storebound console plan play-content-rating
Play Console: Content rating (IARC)
start: https://play.google.com/console
then: Pick the developer account if asked (set play.developerId for a deep link)
then: the app (com.example.prismpuddle) in the app list
then: Policy and programs > App content
then: Content rating > Start (or Manage)
Steps:
1. Open App content > Content ratings (/app-content/content-rating-overview), then "Start questionnaire": it opens /app-content/content-rating-iarc-questionnaire. [click: "Start questionnaire"]
2. Page 1: type the email address and pick the category (material radio: JS click on the inner [role=radio]). The IARC Terms of Use checkbox is an agreement: never tick it; the owner ticks it (it is in the batch). Then Next. [click: "Next"]
3. The questionnaire is ONE long page of sections (Violence, Fear, Sexuality, Gambling, Language, Controlled Substance, Crude Humor, Digital Purchases, Miscellaneous). Answer each section's Yes / No as the sheet says; each Yes reveals follow-ups right below it, so answer top to bottom and re-read the page after every Yes. Run it as one in-page job (plan.inPageLoop) and poll window.__log.
4. Violence follow-ups (after Yes): the humans / non-humans checkboxes, then setting, childlike style, reactions, presentation checkboxes, blood level, historical war setting, innocent characters, fierce sounds.
5. Fear, Sexuality, Gambling, Language, Controlled Substance, Crude Humor follow-ups (after Yes): answer the questions shown as the sheet says (details.<section>.other).
6. Digital Purchases follow-ups (after "purchases of digital goods" Yes): the random items (loot box) question, then trading between players.
7. Miscellaneous: user interaction (after Yes: block, report, moderation, friends only), location sharing, Nazi symbols, Korea history, terrorism, crimes.
8. Press Save (stores the draft; nothing goes to IARC yet), then Next: the Summary shows the rating per authority (PEGI, ESRB, USK, ClassInd, Google Play ...). [click: "Save", "Next"]
9. Summary: stop. Do NOT press its Save (the final button here is "Save", not "Submit"): tell the owner the ratings shown and let them press Save (it applies the IARC ratings).
10. Verify: read the Summary page as text and pipe it to `storebound console verify play-content-rating --from-text -`.
Answer sheet:
[ok] Email address: s***@example.com -> type "Email address" @ Email address and category
[ask] Category: (you decide) -> pick "Category" @ Email address and category
Pick the category; set console.contentRating.category to have it filled.
[you] IARC Terms of Use: the owner ticks it (an agreement) -> untick "IARC Terms of Use" @ Email address and category
An agreement on page 1 (a checkbox): only the owner ticks it, never the agent or storebound.
[ask] Violence: (you decide) -> pick "Does the app contain (any )violen" @ Questionnaire (Does the app contain (any )violen)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.violence.humans, violence.nonHumans, violence.setting, violence.childlike, violence.reactions, violence.presentation, violence.blood, violence.historicalWar, violence.innocents, violence.fierceSounds too.
[ask] Fear / horror: (you decide) -> pick "Does the app contain …scary" @ Questionnaire (Does the app contain …scary)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.fear.other too.
[ask] Sexuality or nudity: (you decide) -> pick "Does the app contain …sexual" @ Questionnaire (Does the app contain …sexual)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.sexuality.other too.
[ask] Language (profanity): (you decide) -> pick "Does the app contain …profan" @ Questionnaire (Does the app contain …profan)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.language.other too.
[ask] Controlled substances (drugs, alcohol, tobacco): (you decide) -> pick "Does the app contain …drug" @ Questionnaire (Does the app contain …drug)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.controlledSubstances.other too.
[ask] Crude humour: (you decide) -> pick "crude humour" @ Questionnaire (crude humour)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.crudeHumor.other too.
[ask] Gambling with real money: (you decide) -> pick "Does the app allow …gambl…real money" @ Questionnaire (Does the app allow …gambl…real money)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.
[ask] Simulated gambling: (you decide) -> pick "simulated gambling" @ Questionnaire (simulated gambling)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.
[ok] Random paid items (loot boxes): No -> pick "No" @ Questionnaire (random (paid )items)
[ask] Users can interact or exchange content: (you decide) -> pick "users …interact" @ Questionnaire (users …interact)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.userInteraction.block, userInteraction.report, userInteraction.moderation, userInteraction.friendsOnly too.
[ask] Shares the user's location with other users: (you decide) -> pick "share …(physical )location" @ Questionnaire (share …(physical )location)
Answer from the app's real content; set console.contentRating.answers to have it filled.
[ask] Digital purchases: (you decide) -> pick "digital goods" @ Questionnaire (digital goods)
Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.digitalPurchases.playerTrading too.
[ask] Unrestricted internet access (browser): (you decide) -> pick "unrestricted internet" @ Questionnaire (unrestricted internet)
Answer from the app's real content; set console.contentRating.answers to have it filled.
[ask] Miscellaneous: Nazi symbols or references?: (you decide) -> pick "nazi" @ Questionnaire: Miscellaneous follow-ups (nazi)
Answer from the app's real content; set console.contentRating.details.miscellaneous.naziSymbols to have it filled.
[ask] Miscellaneous: content about Korean history that may be sensitive?: (you decide) -> pick "korea" @ Questionnaire: Miscellaneous follow-ups (korea)
Answer from the app's real content; set console.contentRating.details.miscellaneous.koreaHistory to have it filled.
[ask] Miscellaneous: terrorism or extremist content?: (you decide) -> pick "terror" @ Questionnaire: Miscellaneous follow-ups (terror)
Answer from the app's real content; set console.contentRating.details.miscellaneous.terrorism to have it filled.
[ask] Miscellaneous: depicts or encourages crimes?: (you decide) -> pick "crime" @ Questionnaire: Miscellaneous follow-ups (crime)
Answer from the app's real content; set console.contentRating.details.miscellaneous.crimes to have it filled.
Never press: "IARC Terms of Use box", "Save (Summary page)", "Publish", "Submit", "Create", "Create app", "Send", "Release", "Definitions and Examples", "?", "More information"
Final button (the owner's): Save (Summary page)
Rule: Press only navigation (Next, Back, Get Started, Edit, Set Up, Start / Manage links) and draft saves (Save draft / Save (questionnaire page)). Never press Save (Summary page) or anything that publishes, submits, creates, sends or accepts; stop and tell the owner.
UI facts:
- Verified 2026-10-12 (an agent with a browser tool on a real game):
- App content > Content ratings is /app-content/content-rating-overview; "Start questionnaire" opens /app-content/content-rating-iarc-questionnaire.
- Page 1: email address, the category radio, and an IARC Terms of Use checkbox: an agreement, the owner ticks it, never the agent.
- The questionnaire is one long page of sections; each Yes reveals follow-ups (Violence: humans / non-humans checkboxes, then setting, childlike style, reactions, presentation checkboxes, blood level, historical war setting, innocent characters, fierce sounds; Digital Purchases: digital goods, then loot boxes, then player trading; Miscellaneous: user interaction then block / report / moderation / friends-only, location sharing, Nazi symbols, Korea history, terrorism, crimes). Follow-up wording here is storebound's: match by topic and the console's own labels.
- Save on the questionnaire stores it; Next goes to the Summary with per-authority ratings. The Summary's final button is "Save" (not "Submit"): the owner's.
- Example result (the game of that run): PEGI 7, ESRB E10+, USK 12, ClassInd 18 (paid random items alone made it 18), Google Play 7+.
- The email address field gets the full app.contactEmail from the config (printed masked here).
- Play Console radios and checkboxes are <material-radio> / <material-checkbox>: computer-tool clicks and setting the native input (form_input) can leave the Angular model unchanged. Click the inner [role=radio] / [role=checkbox] (or the <input> itself) with a JS el.click(), then read aria-checked (or input.checked). Their labels sit in sibling nodes, not in an aria-label: walk up from the control to the nearest parent whose innerText is not empty and match by "starts with" (verified 2026-10-12).
- An app's Play Console home is /console/u/0/developers/<developerId>/app/<appId>/app-dashboard (verified 2026-10-12); with play.appId (or the id storebound remembered) the plan starts there.
Driving:
- Use your own browser tool (Claude in Chrome, a built-in browser, Playwright). Read the page as text or an accessibility tree first; click by visible text; adapt when the page differs from these steps (the console changes without notice), but never press anything in buttons.neverPress.
- Long flows run INSIDE the page: one Runtime.evaluate / javascript call times out at about 45 s on big Play pages (the IARC questionnaire, App content), so never loop over many controls in one call. Paste plan.inPageLoop once per page (one short call; it defines window.__sb), start the job with window.__sb.run([{ role: "radio", label: "No", within: "<question text>" }, { role: "checkbox", label: "<box label>", want: true }, ...]) (it returns at once), then poll window.__log.slice(-20) and window.__done in short calls every 2-5 s until the log says done. The helper only picks radios and ticks boxes (never agreements); press Next / Save yourself afterwards, by their visible text.
- Wait for each step to render (poll for the next heading or input, up to ~10 s) before acting; never act on a step you have not read.
- An answer with status ask-owner: ask the owner in chat before filling it (AGENTS.md rule 5), or leave it empty and tell the owner. Never guess.
- Stop before the final button (buttons.final) and tell the owner exactly what to press; then run the verify command.
- Never sign in, type a password, accept an agreement or tick a declaration: the owner does that in the browser.
In-page helper (plan.inPageLoop: paste it once per page in one short call, then window.__sb.run([...]) and poll window.__log):
(() => {
const norm = (t) => (t || "").replace(/\s+/g, " ").trim();
const log = (m) => { window.__log.push(new Date().toISOString().slice(11, 19) + " " + m); };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const labelOf = (el) => {
const own = norm(el.getAttribute("aria-label"));
if (own) return own;
for (let a = el; a && a !== document.body; a = a.parentElement) { const t = norm(a.innerText); if (t) return t; }
return "";
};
const NEVER = /terms of use|terms of service|i agree|agree to|declaration|developer program polic|export laws/i;
const CONTROLS = "[role=radio], [role=checkbox], input[type=radio], input[type=checkbox]";
const scopeOf = (text) => {
const want = norm(text).toLowerCase(); let best = null;
for (const el of document.querySelectorAll("section, fieldset, div, [role=radiogroup], [role=group]"))
if (norm(el.innerText).toLowerCase().includes(want) && el.querySelector(CONTROLS) && (!best || best.contains(el))) best = el;
return best;
};
const find = (role, label, scope) => {
const want = norm(label).toLowerCase();
const hits = [...(scope || document).querySelectorAll("[role=" + role + "], input[type=" + role + "]")]
.filter((el) => el.getClientRects().length && labelOf(el).toLowerCase().startsWith(want));
return hits.length === 1 ? hits[0] : null;
};
const set = async (s) => {
const scope = s.within ? scopeOf(s.within) : null;
if (s.within && !scope) return "no block with \"" + s.within + "\"";
const el = find(s.role, s.label, scope);
if (!el) return "not found or ambiguous";
if (NEVER.test(labelOf(el))) return "refused: an agreement (the owner ticks it)";
const host = el.closest("material-radio, material-checkbox");
const inner = el.matches("[role=radio], [role=checkbox]") ? el : (host && host.querySelector("[role=radio], [role=checkbox]")) || el;
const state = () => (inner.hasAttribute("aria-checked") ? inner.getAttribute("aria-checked") === "true" : !!inner.checked);
const want = s.want !== false;
if (state() !== want) inner.click();
await sleep(150);
return state() === want ? "ok" : "did not change (read the control, try the <input> itself)";
};
window.__log = []; window.__done = false;
window.__sb = { log, sleep, labelOf, scopeOf, find, set };
window.__sb.run = (steps) => {
window.__done = false;
window.__run = (async () => {
for (let i = 0; i < steps.length; i++) {
const s = steps[i], tag = (i + 1) + "/" + steps.length + " " + s.role + " \"" + s.label + "\"" + (s.within ? " in \"" + s.within + "\"" : "");
try { log(tag + ": " + await set(s)); } catch (e) { log(tag + ": error " + e.message); }
}
log("done"); window.__done = true;
})();
return "started " + steps.length + " steps: poll window.__log";
};
return "storebound helper ready: window.__sb.run([...]), then poll window.__log.slice(-20) and window.__done";
})()
Verify: storebound console verify play-content-rating --from-text -
- The Summary shows each question with the Yes / No of the answer sheet, and a rating per authority.
Ask the owner first:
- Category: ask the owner (Pick the category; set console.contentRating.category to have it filled.)
- IARC Terms of Use: an agreement, the owner ticks it
- Violence: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.violence.humans, violence.nonHumans, violence.setting, violence.childlike, violence.reactions, violence.presentation, violence.blood, violence.historicalWar, violence.innocents, violence.fierceSounds too.)
- Fear / horror: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.fear.other too.)
- Sexuality or nudity: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.sexuality.other too.)
- Language (profanity): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.language.other too.)
- Controlled substances (drugs, alcohol, tobacco): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.controlledSubstances.other too.)
- Crude humour: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.crudeHumor.other too.)
- Gambling with real money: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.)
- Simulated gambling: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.gambling.other too.)
- Users can interact or exchange content: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.userInteraction.block, userInteraction.report, userInteraction.moderation, userInteraction.friendsOnly too.)
- Shares the user's location with other users: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled.)
- Digital purchases: ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled. If Yes, the page asks follow-ups: set console.contentRating.details.digitalPurchases.playerTrading too.)
- Unrestricted internet access (browser): ask the owner (Answer from the app's real content; set console.contentRating.answers to have it filled.)
- Miscellaneous: Nazi symbols or references?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.naziSymbols to have it filled.)
- Miscellaneous: content about Korean history that may be sensitive?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.koreaHistory to have it filled.)
- Miscellaneous: terrorism or extremist content?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.terrorism to have it filled.)
- Miscellaneous: depicts or encourages crimes?: ask the owner (Answer from the app's real content; set console.contentRating.details.miscellaneous.crimes to have it filled.)

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -0,0 +1,10 @@
$ storebound play products --dry
[play] 1/2 com.example.app.no_ads: would create · 174 regions · 2 listings (dry)
[play] 2/2 com.example.app.gems_100: would create · 174 regions · 2 listings (dry)
dry run: play products: 2 change(s); nothing written
item action detail
------------------------ ------ ---------------------------------
com.example.app.no_ads create 174 regions, 2 listings, USD 2.99
com.example.app.gems_100 create 174 regions, 2 listings, USD 0.99
[play] not on Google Play: com.example.app.no_ads, com.example.app.gems_100. If you created them in the last minutes, the store's list can lag a minute after creating; re-run before acting on this plan
[play] products checked (dry run, nothing written)

BIN
site/img/term/products.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,59 @@
$ storebound release plan 1.0.0
[release] Example App: validate (read-only, both stores)
Example App 1.0.0
both stores (fix first):
1. CONFIG App Review needs a demo account (sign-in required) · add reviewDemoAccount to .storebound/credentials.json · review-access skill
-> storebound check
2. CONFIG missing file: store/icon/icon.png
-> storebound check
3. CONFIG missing file: store/icon/foreground.png
-> storebound check
4. CONFIG missing file: store/icon/background.png
-> storebound check
App Store: not on the store
5. STORE 1.0.0 is not on App Store yet: upload a build (Xcode / Transporter), then create the version with its texts
-> storebound apple next-build; storebound apple listing --version 1.0.0 --build <N> --dry
6. BLOCKER no App Store version being prepared
-> storebound apple listing --version x.y.z
7. BLOCKER no build attached to the version
-> storebound apple listing --version x.y.z --build N (pick the build number you just uploaded)
8. BLOCKER en-US: no localization on the version
-> storebound apple listing
9. BLOCKER tr: no localization on the version
-> storebound apple listing
10. BLOCKER no App Review information
-> storebound apple review-details
11. BLOCKER age rating declaration not readable / missing
-> storebound apple appinfo
12. BLOCKER no app price schedule
-> storebound apple appinfo
13. BLOCKER sign-in required for review but no demo account in .storebound/credentials.json (reviewDemoAccount) or REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD
-> review-access skill
14. BLOCKER 2.1(a): Sign-in is required but there is no demo account for App Review (storebound.config.ts:35)
-> Create a demo account that sees every feature (not a sign-up the reviewer must do), put it in .storebound/credentials.json as reviewDemoAccount (never in the repo), then `storebound apple review-details`. · review-access skill
15. warn 2.1(a): Placeholder or template text in the store listing (store/appstore.md:20)
-> Replace the template lines and any lorem ipsum / TODO with the real copy before you push the listing. · aso-basics skill
Google Play: not on the store
16. STORE 1.0.0 is not on Google Play yet: upload the AAB to a testing track
-> storebound play next-version-code; storebound play release <aab> --internal completed --dry
17. BLOCKER no listing for the default language en-US
-> storebound play listing
18. BLOCKER en-US: no listing
-> storebound play listing
19. BLOCKER tr-TR: no listing
-> storebound play listing
20. BLOCKER no release on any track
-> storebound play release <aab>
21. BLOCKER sign-in required for review but no demo account in .storebound/credentials.json (reviewDemoAccount) or REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD
-> review-access skill
22. BLOCKER Play: Package name: Package name com.example.app uses the reserved com.example prefix (storebound.config.ts:46)
-> Pick your own reverse-domain package name (it can never change after the first upload) and set it in the Gradle applicationId and play.packageName. · play-first-release skill
23. warn production has 0 countries / regions (the API cannot set them)
-> storebound console plan play-production-countries
24. warn sign-in required: Play Console > App content > App access must list the demo credentials and steps (no API)
-> review-access skill
25. warn Play: App access: Sign-in is required but there are no reviewer credentials for Play App access (storebound.config.ts:35)
-> Play Console > App content > App access: add the demo account and instructions (keep the credentials in .storebound/credentials.json only). · review-access skill
26. warn Play: Metadata: Placeholder or template text in the store listing (store/listing.md:23)
-> Replace the template lines and any lorem ipsum / TODO with the real copy before you push the listing. · aso-basics skill

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

37
site/img/term/scan.txt Normal file
View file

@ -0,0 +1,37 @@
$ storebound scan
storebound scan · storebound.config.ts
stack: iOS (native) · targets: iOS · 11 files
SDKs
Google AdMob ads, tracking-capable · CocoaPods Google-Mobile-Ads-SDK 11.10.0 (Podfile.lock:2)
Google Sign-In auth · CocoaPods GoogleSignIn 7.1.0 (Podfile.lock:3)
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
fix: Create a demo account that sees every feature (not a sign-up the reviewer must do), put it in .storebound/credentials.json as reviewDemoAccount (never in the repo), then `storebound apple review-details`.
skill: review-access · https://developer.apple.com/app-store/review/guidelines/
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 {
fix: Add "Delete account" in the app (it deletes the account and its data, not only signs out; with Sign in with Apple also revoke the token), then set accounts.inApp to where it starts, e.g. "Settings > Account > Delete account".
skill: account-deletion · https://developer.apple.com/app-store/review/guidelines/
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 }
fix: Offer Sign in with Apple (Signing & Capabilities > Sign in with Apple, ASAuthorizationAppleIDProvider) or another login that limits data to name and email, lets people hide their email and does not track. Not needed when the app only uses your own account system, or for the exemptions in 4.8 (education / enterprise accounts, government ID, a client for that one service).
skill: review-rejections · https://developer.apple.com/app-store/review/guidelines/
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)
fix: Add a visible "Restore Purchases" button (StoreKit 2: AppStore.sync(); StoreKit 1: restoreCompletedTransactions; RevenueCat: restorePurchases) on the store / paywall screen or in Settings.
skill: store-iap-setup · https://developer.apple.com/app-store/review/guidelines/
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)
fix: If any SDK tracks (personalized ads, attribution, IDFA): add NSUserTrackingUsageDescription and request ATT before the SDKs start, and answer App Privacy "used for tracking". If nothing tracks: configure the ad SDKs for non-personalized ads and answer "no tracking".
skill: ads-mediation-checks · https://developer.apple.com/app-store/review/guidelines/
… 52 more lines: the declarations proposal (Data safety, App Privacy, ATT, privacy manifest) and the config diff

BIN
site/img/term/scan.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB