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:
CRTkafa 2026-10-13 07:25:01 +08:00
parent d2ce185441
commit e5f148e534
55 changed files with 1648 additions and 158 deletions

View file

@ -276,12 +276,12 @@ export function candidates(i: CandidateInput): Candidate[] {
// ---------------------------------------------------------------------------------------------------------------
// The 100-byte keyword field
export interface FieldProposal { locale: string; before: string; after: string; bytes: number; kept: string[]; added: string[]; dropped: { term: string; why: string }[] }
export interface FieldProposal { locale: string; before: string; after: string; chars: number; kept: string[]; added: string[]; dropped: { term: string; why: string }[] }
const bytes = (s: string) => Buffer.byteLength(s, 'utf8');
const chars = (s: string) => [...s].length;
/**
* A keyword field for one locale that fits 100 bytes (UTF-8) with the existing rules: commas without spaces, no
* A keyword field for one locale that fits 100 characters (App Store Connect counts characters, not bytes) with the existing rules: commas without spaces, no
* duplicates (plural-insensitive), no words of the name / subtitle, no filler or category words, single words. Current
* terms that are still valid are kept first (change a few terms per release, not all), unless `fresh`; then the best
* candidates fill the room. Pure.
@ -292,7 +292,7 @@ export function fitKeywordField(o: { locale: string; current: string; name: stri
const blocked = new Map(o.ranked.filter(c => c.status !== 'candidate' && c.kind === 'word').map(c => [stem(c.term, L), c.why ?? c.status] as const));
const out: string[] = [], kept: string[] = [], added: string[] = [], dropped: { term: string; why: string }[] = [];
const seen: string[] = [];
const fits = (t: string) => bytes([...out, t].join(',')) <= limit;
const fits = (t: string) => chars([...out, t].join(',')) <= limit;
const tryAdd = (t: string, into: string[]): string | null => {
const ws = words(t, L);
if (!ws.length) return 'empty';
@ -301,12 +301,12 @@ export function fitKeywordField(o: { locale: string; current: string; name: stri
if (ws.length === 1 ? coveredBy(ws[0]!, seen, L) : seen.includes(ws.join(' '))) return 'duplicate';
const b = ws.length === 1 ? blocked.get(stem(ws[0]!, L)) : undefined;
if (b) return b;
if (!fits(t)) return 'no room in 100 bytes';
if (!fits(t)) return 'no room in 100 characters';
seen.push(ws.length === 1 ? ws[0]! : ws.join(' ')); out.push(t); into.push(t);
return null;
};
if (!o.fresh) for (const t of o.current.split(',').map(s => s.trim().toLocaleLowerCase(L)).filter(Boolean)) {
// keep a current phrase as the words it is made of: single words combine on their own and save bytes
// keep a current phrase as the words it is made of: single words combine on their own and save characters
const parts = /\s/.test(t) ? tokens(t, L) : [t];
for (const p of parts) { const why = tryAdd(p, kept); if (why && why !== 'duplicate') dropped.push({ term: p, why }); }
}
@ -315,7 +315,7 @@ export function fitKeywordField(o: { locale: string; current: string; name: stri
tryAdd(c.term, added);
}
const after = out.join(',');
return { locale: L, before: o.current, after, bytes: bytes(after), kept, added, dropped };
return { locale: L, before: o.current, after, chars: chars(after), kept, added, dropped };
}
// ---------------------------------------------------------------------------------------------------------------