desktop: a Fix button on scan and validate findings storebound can fix itself. Findings carry action { method, args } (cli/lib/fix-actions.ts); privacy-labels runs scan.write (scan --write-only, the dialog shows the config diff), Play listing gaps play.listing, a missing App Store localization or copyright apple.listing (editable version only), the App Review contact apple.review_details; each a dry run, the native dialog, then the confirmed run. --write-only is forbidden for MCP tools; tests pin the mapping, the round trip on a fixture and that no fix reaches a submit, release or publish
This commit is contained in:
parent
bd83d036ba
commit
992f5bcc0e
18 changed files with 257 additions and 37 deletions
|
|
@ -1,6 +1,8 @@
|
|||
// Store readiness rules for `storebound validate`. Pure: the command collects a snapshot from both APIs (read-only),
|
||||
// these functions turn it into an ordered list of findings. Order = the order you would fix things in.
|
||||
|
||||
import { type FixAction, fixAppleListing, fixPlayListing, fixReviewDetails } from './fix-actions';
|
||||
|
||||
export type Level = 'blocker' | 'warn' | 'ok';
|
||||
|
||||
/** Screenshot sets App Store Connect takes instead of a required one (App Store Connect Help, "Screenshot
|
||||
|
|
@ -13,7 +15,9 @@ export const ACCEPTED_INSTEAD: Record<string, string[]> = {
|
|||
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 Finding { store: 'apple' | 'play'; level: Level; code: string; msg: string; fix?: string;
|
||||
/** The desktop's Fix button: a write fully determined by an existing command (lib/fix-actions.ts). */
|
||||
action?: FixAction }
|
||||
|
||||
export interface UrlCheck { url: string; status: number | null; error?: string; body?: string }
|
||||
|
||||
|
|
@ -168,12 +172,14 @@ export function copyrightRules(s: Pick<AppleSnapshot, 'version' | 'configCopyrig
|
|||
const live = s.version.copyright?.trim();
|
||||
const cfg = s.configCopyright?.trim();
|
||||
if (live) return [{ store: 'apple', level: cfg ? 'ok' : 'warn', code: 'copyright', msg: cfg ? `copyright on the store: ${live}` : `copyright on the store: ${live}; ${COPYRIGHT_MISSING}`, fix: cfg ? undefined : 'set app.copyright in storebound.config.ts' }];
|
||||
return [{ store: 'apple', level: 'blocker', code: 'copyright', msg: `version ${s.version.versionString} has no copyright on the store${cfg ? ` (config has '${cfg}': storebound apple listing sends it)` : `; ${COPYRIGHT_MISSING}`}`, fix: cfg ? 'storebound apple listing --version x.y.z' : 'set app.copyright in storebound.config.ts, then storebound apple listing' }];
|
||||
return [{ store: 'apple', level: 'blocker', code: 'copyright', msg: `version ${s.version.versionString} has no copyright on the store${cfg ? ` (config has '${cfg}': storebound apple listing sends it)` : `; ${COPYRIGHT_MISSING}`}`, fix: cfg ? 'storebound apple listing --version x.y.z' : 'set app.copyright in storebound.config.ts, then storebound apple listing',
|
||||
...(cfg && EDITABLE.includes(s.version.state) ? { action: fixAppleListing(s.version.versionString) } : {}) }];
|
||||
}
|
||||
|
||||
export function appleRules(s: AppleSnapshot): Finding[] {
|
||||
const f: Finding[] = [];
|
||||
const add = (level: Level, code: string, msg: string, fix?: string) => f.push({ store: 'apple', level, code, msg, fix });
|
||||
const add = (level: Level, code: string, msg: string, fix?: string, action?: FixAction) => f.push({ store: 'apple', level, code, msg, fix, ...(action ? { action } : {}) });
|
||||
const listing = s.version && EDITABLE.includes(s.version.state) ? fixAppleListing(s.version.versionString) : undefined;
|
||||
|
||||
if (!s.version) add('blocker', 'version', 'no App Store version being prepared', 'storebound apple listing --version x.y.z');
|
||||
else if (EDITABLE.includes(s.version.state)) add('ok', 'version', `version ${s.version.versionString}: ${s.version.state}`);
|
||||
|
|
@ -193,7 +199,7 @@ export function appleRules(s: AppleSnapshot): Finding[] {
|
|||
}
|
||||
|
||||
const have = new Map(s.locales.map(l => [l.locale, l] as const));
|
||||
for (const loc of s.expectedLocales) if (!have.has(loc)) add('blocker', 'locale', `${loc}: no localization on the version`, 'storebound apple listing');
|
||||
for (const loc of s.expectedLocales) if (!have.has(loc)) add('blocker', 'locale', `${loc}: no localization on the version`, 'storebound apple listing', listing);
|
||||
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');
|
||||
|
|
@ -207,10 +213,10 @@ export function appleRules(s: AppleSnapshot): Finding[] {
|
|||
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`);
|
||||
|
||||
const r = s.reviewDetail;
|
||||
if (!r) add('blocker', 'review-details', 'no App Review information', 'storebound apple review-details');
|
||||
if (!r) add('blocker', 'review-details', 'no App Review information', 'storebound apple review-details', fixReviewDetails());
|
||||
else {
|
||||
const miss = (['contactFirstName', 'contactLastName', 'contactPhone', 'contactEmail'] as const).filter(k => !r[k]);
|
||||
if (miss.length) add('blocker', 'review-details', `review contact missing ${miss.join(', ')}`, 'storebound apple review-details');
|
||||
if (miss.length) add('blocker', 'review-details', `review contact missing ${miss.join(', ')}`, 'storebound apple review-details', fixReviewDetails());
|
||||
else if (r.demoAccountRequired && !r.demoAccountName) add('blocker', 'review-details', 'demo account required but not given');
|
||||
else add('ok', 'review-details', 'review contact set');
|
||||
if (!r.notes) add('warn', 'review-notes', 'no review notes: say where IAPs, Restore Purchases, ads and the privacy policy are', 'apple.info.reviewNotes');
|
||||
|
|
@ -257,14 +263,14 @@ export const normAdsLine = (l: string) => l.split('#')[0]!.split(',').map(p => p
|
|||
|
||||
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 add = (level: Level, code: string, msg: string, fix?: string, action?: FixAction) => f.push({ store: 'play', level, code, msg, fix, ...(action ? { action } : {}) });
|
||||
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');
|
||||
if (!have.has(s.defaultLanguage)) add('blocker', 'listing', `no listing for the default language ${s.defaultLanguage}`, 'storebound play listing', fixPlayListing());
|
||||
for (const lang of s.expectedLanguages) if (!have.has(lang)) add('blocker', 'listing', `${lang}: no listing`, 'storebound play listing', fixPlayListing());
|
||||
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');
|
||||
if (miss.length) add('blocker', 'listing', `${l.language}: missing ${miss.join(', ')}`, 'storebound play listing', fixPlayListing());
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue