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

@ -27,12 +27,13 @@ export function auditAppleKeywords(locale: string, c: AppleCopy, appName: string
const at = `apple ${locale}`;
const kw = c.keywords ?? '';
if (!kw) return [{ level: 'error', where: at, msg: 'keywords: empty' }];
// App Store Connect limits the keyword field to 100 bytes (UTF-8), not characters: Cyrillic, Arabic, Hindi, Thai,
// Japanese, Korean and Chinese words take 2-3 bytes per character, so both the limit and the fill hint count bytes.
const bytes = Buffer.byteLength(kw, 'utf8');
if (bytes > 100) out.push({ level: 'error', where: at, msg: `keywords: ${bytes} bytes > 100` });
// App Store Connect limits the keyword field to 100 characters, not UTF-8 bytes: live versions (READY_FOR_SALE,
// read 2026-10-13) hold Thai 95 characters / 265 bytes, Russian 91 / 169, Turkish 100 / 105, Polish 99 / 105
// (store-pitfalls 43). Both the limit and the fill hint count characters.
const chars = len(kw);
if (chars > 100) out.push({ level: 'error', where: at, msg: `keywords: ${chars} characters > 100` });
if (/,\s|\s,/.test(kw)) out.push({ level: 'error', where: at, msg: 'keywords: spaces around commas waste characters' });
if (bytes < 90) out.push({ level: 'warn', where: at, msg: `keywords: ${bytes}/100 bytes used; fill at least 90 (${100 - bytes} bytes free)` });
if (chars < 90) out.push({ level: 'warn', where: at, msg: `keywords: ${chars}/100 characters used; fill at least 90 (${100 - chars} characters free)` });
const terms = kw.split(',').map(t => t.trim().toLocaleLowerCase(locale)).filter(Boolean);
const seen = new Set<string>();
for (const t of terms) {