console plans: guardrails from a real run (2026-10-21): health none label and Sign in details Yes/No + Add details (old labels as altLabels, verify accepts both), finance boxes on step 1, App Privacy Additional Setup Required OK, verify from DOM input states (verify.capture, --from), Save + confirm before every Next on Play pages, IARC Digital purchases boxes and an exact-label in-page helper that refuses patterns and blanket answers; review contact found in parent folders, the profile and more key shapes with the files checked listed; one-line errors for missing required values (Args.required); pitfalls 86-87, tests
This commit is contained in:
parent
a6dab8f3aa
commit
335a4124ab
23 changed files with 486 additions and 74 deletions
|
|
@ -212,11 +212,65 @@ export function ascKey(root: string): AscKey {
|
|||
return { keyId, issuerId, pem: inline(p8, /BEGIN PRIVATE KEY/) };
|
||||
}
|
||||
|
||||
/** The keys and nestings a review contact is read from (first hit): the documented one, then shapes seen in real files. */
|
||||
export const REVIEW_CONTACT_KEYS = ['reviewContact', 'apple.reviewContact', 'appReview.contact', 'review.contact', 'reviewDetails.contact', 'contact'] as const;
|
||||
|
||||
const pickStr = (o: Record<string, unknown>, ...ks: string[]): string => { for (const k of ks) { const v = o[k]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; };
|
||||
|
||||
/** A contact object in any accepted field spelling (firstName / first_name / contactFirstName; a "name" is split). */
|
||||
export function contactOf(raw: unknown): ReviewContact | null {
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
||||
const o = raw as Record<string, unknown>;
|
||||
const email = pickStr(o, 'email', 'contactEmail', 'emailAddress');
|
||||
if (!email || isPlaceholder(email, { example: false })) return null;
|
||||
let firstName = pickStr(o, 'firstName', 'first_name', 'contactFirstName', 'givenName'), lastName = pickStr(o, 'lastName', 'last_name', 'contactLastName', 'familyName');
|
||||
const name = pickStr(o, 'name', 'fullName');
|
||||
if (!firstName && !lastName && name) { const parts = name.split(/\s+/); firstName = parts[0] ?? ''; lastName = parts.slice(1).join(' '); }
|
||||
return { firstName, lastName, phone: pickStr(o, 'phone', 'contactPhone', 'phoneNumber'), email };
|
||||
}
|
||||
|
||||
const atPath = (o: Store, path: string): unknown => path.split('.').reduce<unknown>((x, k) => (x && typeof x === 'object' ? (x as Store)[k] : undefined), o);
|
||||
|
||||
/** Where a review contact is looked up, in order: the project's .storebound/credentials.json, the same file in each
|
||||
* parent folder up to the repository root (a config in a subfolder), then the project's credential profile file. */
|
||||
function contactFiles(root: string): string[] {
|
||||
const out: string[] = [];
|
||||
let d = resolve(root);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
out.push(join(d, '.storebound', 'credentials.json'));
|
||||
if (existsSync(join(d, '.git'))) break;
|
||||
const up = resolve(d, '..');
|
||||
if (up === d || up === homedir()) break;
|
||||
d = up;
|
||||
}
|
||||
try { const p = activeProfile(root); if (p && PROFILE_RE.test(p.name)) out.push(join(profileDir(), `${p.name}.json`)); } catch { /* unreadable project file: listed below */ }
|
||||
return out;
|
||||
}
|
||||
|
||||
/** App Review contact (personal data, never in the config), the file and key it came from, and every place checked. */
|
||||
export function findReviewContact(root: string): { contact: ReviewContact | null; from: string | null; checked: string[] } {
|
||||
const checked: string[] = [];
|
||||
for (const f of contactFiles(root)) {
|
||||
if (!existsSync(f)) { checked.push(`${f} (missing)`); continue; }
|
||||
let st: Store;
|
||||
try { st = JSON.parse(readFileSync(f, 'utf8')) as Store; } catch (e) { checked.push(`${f} (unreadable: ${(e as Error).message})`); continue; }
|
||||
for (const k of REVIEW_CONTACT_KEYS) {
|
||||
const c = contactOf(atPath(st, k));
|
||||
if (c) return { contact: c, from: `${f} ${k}`, checked };
|
||||
}
|
||||
checked.push(`${f} (keys ${REVIEW_CONTACT_KEYS.join(', ')}: none with an email)`);
|
||||
}
|
||||
return { contact: null, from: null, checked };
|
||||
}
|
||||
|
||||
/** The error when no review contact is found: every file and key that was checked, and the fix. */
|
||||
export function noContactMessage(root: string, checked: string[] = findReviewContact(root).checked): string {
|
||||
return `no review contact. Checked: ${checked.join('; ') || '(nothing)'}. Add "reviewContact": { "firstName", "lastName", "phone", "email" } to ${join(resolve(root), '.storebound', 'credentials.json')}, or pass --contact-from <Apple ID of another app>`;
|
||||
}
|
||||
|
||||
/** App Review contact from .storebound/credentials.json (personal data, so never in the config). */
|
||||
export function reviewContact(root: string): ReviewContact | null {
|
||||
const c = fileStore(root).reviewContact as Partial<ReviewContact> | undefined;
|
||||
if (!c?.email || isPlaceholder(c.email, { example: false })) return null;
|
||||
return { firstName: c.firstName ?? '', lastName: c.lastName ?? '', phone: c.phone ?? '', email: c.email };
|
||||
return findReviewContact(root).contact;
|
||||
}
|
||||
|
||||
export interface DemoAccount { username: string; password: string }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue