apple appinfo: live app price and availability compared with the config (price change needs --allow-price-changes, territory removals --allow-availability-changes); review-details --dry reads the store and shows old -> new (masked)

This commit is contained in:
CRTkafa 2026-10-22 20:33:46 +08:00
parent f7ff52dfdd
commit f2b841d735
3 changed files with 168 additions and 13 deletions

View file

@ -8,12 +8,30 @@
import { refuseTemplate } from '../lib/template-answers';
import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { findReviewContact, noContactMessage, reviewDemoAccount } from '../lib/credentials';
import { findReviewContact, maskSecret, maskUser, noContactMessage, reviewDemoAccount } from '../lib/credentials';
import { reviewDetailPlan } from '../lib/review-details';
import { done, log } from '../lib/log';
import { createOnce } from '../lib/upsert';
import { ascClient } from './asc';
type Attrs = Record<string, unknown>;
const shown = (k: string, v: unknown): string => {
if (v === null || v === undefined || v === '') return '(empty)';
const t = String(v);
if (k === 'demoAccountPassword') return maskSecret(t);
if (k === 'demoAccountName' || k === 'contactEmail') return maskUser(t);
if (k === 'contactPhone') return `${t.slice(0, 3)}***`;
if (k === 'notes') return `${new TextEncoder().encode(t).length} bytes`;
return t;
};
/** Changed review-detail keys as `key: old -> new` lines (secrets, phone, email masked; notes as byte counts). A missing
* detail (cur null) lists every key as new. Pure. */
export function reviewDetailDiff(cur: Attrs | null, want: Attrs): string[] {
return Object.entries(want).filter(([k, v]) => !cur || (cur[k] ?? null) !== (v ?? null))
.map(([k, v]) => `${k}: ${cur ? shown(k, cur[k]) : '(none)'} -> ${shown(k, v)}`);
}
export async function run(ctx: Ctx): Promise<void> {
const { config, root } = ctx.loaded;
refuseTemplate(config, ['reviewNotes', 'reviewAccess', 'accounts'], 'apple review-details (App Review notes, demo account requirement)');
@ -40,9 +58,14 @@ export async function run(ctx: Ctx): Promise<void> {
if (!contact) throw new Error(noContactMessage(root, checked));
const { attributes, summary } = reviewDetailPlan(config, contact, reviewDemoAccount(root));
if (ctx.args.has('dry')) { log('apple', `would set review details (contact ${from ? 'copied from ' + from : 'from credentials'}, ${summary})`); return; }
const read = async () => (await c.maybe(`/v1/appStoreVersions/${version.id}/appStoreReviewDetail`))?.data ?? null;
const cur = await read();
if (ctx.args.has('dry')) {
const lines = reviewDetailDiff(cur?.attributes ?? null, attributes);
log('apple', `would ${cur ? (lines.length ? 'update' : 'keep') : 'create'} review details (contact ${from ? 'copied from ' + from : 'from credentials'}, ${summary})`);
for (const l of lines) console.log(` ${l}`);
return;
}
// one review detail per version: only changed fields are sent; a new one is created once (a 409 finds it)
const changed = cur ? Object.fromEntries(Object.entries(attributes).filter(([k, v]) => (cur.attributes?.[k] ?? null) !== (v ?? null))) : attributes;
if (cur && !Object.keys(changed).length) { done('apple', `review details on ${version.attributes.versionString} up to date (${summary})`); return; }