Onboarding live apps without owner questions for what the stores and the code prove: storebound onboard imports the live products and subscriptions with USD / base prices (catalog diff clean), contact values, the App Review contact and demo account (credentials.json, masked), sign-in for review from Apple's review detail and auth code, and the account deletion page from the app's site; App Store keywords count characters (live fields up to 265 bytes accepted); validate accepts the 6.5-inch iPhone set and Play's default-language graphics fallback; Play Reporting API off gives the project's enable link and apps list falls back to local apps; accounts default <profile>; store-only apps (apps add --store --app-id) shown read-only in the desktop (pitfalls 43-52)
This commit is contained in:
parent
d2ce185441
commit
e5f148e534
55 changed files with 1648 additions and 158 deletions
|
|
@ -2,6 +2,17 @@
|
|||
// these functions turn it into an ordered list of findings. Order = the order you would fix things in.
|
||||
|
||||
export type Level = 'blocker' | 'warn' | 'ok';
|
||||
|
||||
/** Screenshot sets App Store Connect takes instead of a required one (App Store Connect Help, "Screenshot
|
||||
* specifications", checked 2026-10-13): iPhone needs the 6.9" set or, without it, the 6.5" set; iPad needs the 13" set
|
||||
* (either 13" display type). The smaller sizes are scaled down from these. A live app with only the 6.5" set is fine
|
||||
* (store-pitfalls 44). */
|
||||
export const ACCEPTED_INSTEAD: Record<string, string[]> = {
|
||||
APP_IPHONE_67: ['APP_IPHONE_65'],
|
||||
APP_IPHONE_65: ['APP_IPHONE_67'],
|
||||
APP_IPAD_PRO_3GEN_129: ['APP_IPAD_PRO_129'],
|
||||
APP_IPAD_PRO_129: ['APP_IPAD_PRO_3GEN_129'],
|
||||
};
|
||||
export interface Finding { store: 'apple' | 'play'; level: Level; code: string; msg: string; fix?: string }
|
||||
|
||||
export interface UrlCheck { url: string; status: number | null; error?: string; body?: string }
|
||||
|
|
@ -154,7 +165,12 @@ export function appleRules(s: AppleSnapshot): Finding[] {
|
|||
for (const l of s.locales) {
|
||||
const miss = (['description', 'keywords', 'subtitle'] as const).filter(k => !l[k]);
|
||||
if (miss.length) add('blocker', 'metadata', `${l.locale}: missing ${miss.join(', ')}`, 'store/appstore.md, then storebound apple listing');
|
||||
for (const t of s.requiredDisplayTypes) if (!l.screenshots[t]) add('blocker', 'screenshots', `${l.locale}: no ${t} screenshots`, 'storebound apple screenshots');
|
||||
for (const t of s.requiredDisplayTypes) {
|
||||
if (l.screenshots[t]) continue;
|
||||
const alt = (ACCEPTED_INSTEAD[t] ?? []).find(x => l.screenshots[x]);
|
||||
if (alt) add('warn', 'screenshots-size', `${l.locale}: no ${t} set; Apple accepts the ${alt} set (${l.screenshots[alt]} screenshots) and scales it for the other sizes. Recommended, not required: add ${t}`, 'storebound apple screenshots');
|
||||
else add('blocker', 'screenshots', `${l.locale}: no ${t} screenshots${ACCEPTED_INSTEAD[t] ? ` (or ${ACCEPTED_INSTEAD[t]!.join(' / ')})` : ''}`, 'storebound apple screenshots');
|
||||
}
|
||||
}
|
||||
if (s.locales.length && !f.some(x => x.code === 'metadata' || x.code === 'screenshots' || x.code === 'locale')) add('ok', 'metadata', `${s.locales.length} locales with text and screenshots`);
|
||||
|
||||
|
|
@ -209,14 +225,27 @@ export function playRules(s: PlaySnapshot): Finding[] {
|
|||
const f: Finding[] = [];
|
||||
const add = (level: Level, code: string, msg: string, fix?: string) => f.push({ store: 'play', level, code, msg, fix });
|
||||
const have = new Map(s.listings.map(l => [l.language, l] as const));
|
||||
const fallback: Record<string, string[]> = {};
|
||||
if (!have.has(s.defaultLanguage)) add('blocker', 'listing', `no listing for the default language ${s.defaultLanguage}`, 'storebound play listing');
|
||||
for (const lang of s.expectedLanguages) if (!have.has(lang)) add('blocker', 'listing', `${lang}: no listing`, 'storebound play listing');
|
||||
for (const l of s.listings) {
|
||||
const miss = (['title', 'shortDescription', 'fullDescription'] as const).filter(k => !l[k]);
|
||||
if (miss.length) add('blocker', 'listing', `${l.language}: missing ${miss.join(', ')}`, 'storebound play listing');
|
||||
for (const [type, n] of Object.entries(playRequiredImages(s.tablets))) if ((l.images[type] ?? 0) < n)
|
||||
for (const [type, n] of Object.entries(playRequiredImages(s.tablets))) {
|
||||
if ((l.images[type] ?? 0) >= n) continue;
|
||||
// Play shows the default language's graphics in a language that has none of its own (store-pitfalls 45): only
|
||||
// the default language's missing graphics block; a language with fewer than needed but more than none is
|
||||
// a real gap (Play takes them as they are).
|
||||
const def = have.get(s.defaultLanguage);
|
||||
if (l.language !== s.defaultLanguage && !(l.images[type] ?? 0) && def && (def.images[type] ?? 0) >= n) {
|
||||
(fallback[type] ??= []).push(l.language);
|
||||
continue;
|
||||
}
|
||||
add('blocker', 'images', `${l.language}: ${type} ${l.images[type] ?? 0}/${n}${PLAY_TABLET_IMAGES[type] ? ' (play.tablets is on)' : ''}`, 'storebound play images');
|
||||
}
|
||||
}
|
||||
for (const [type, langs] of Object.entries(fallback))
|
||||
add('ok', 'images-fallback', `${type}: ${langs.length} language(s) without their own use the default language's (${s.defaultLanguage}) graphics: ${langs.slice(0, 6).join(', ')}${langs.length > 6 ? ' ...' : ''}`);
|
||||
if (s.listings.length && !f.some(x => x.code === 'listing' || x.code === 'images')) add('ok', 'listing', `${s.listings.length} listings with text and graphics`);
|
||||
|
||||
add(urlOk(s.privacyUrl) ? 'ok' : 'blocker', 'privacy-url', `privacy policy ${s.privacyUrl.url}: ${urlMsg(s.privacyUrl)}`, 'privacy-policy skill');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue