console: Play App content card Saves are the agent's (declaration-save; Send changes for review, Submit, Publish, Restart review stay forbidden), one owner batch per plan with the IARC Terms tick before the run, console plan play-app-content --all, New App fields typed not set, play.appId from the Create app redirect (console verify --url --write) and storebound play app-id across app list pages
This commit is contained in:
parent
1366e7e231
commit
f741c15829
29 changed files with 1029 additions and 165 deletions
|
|
@ -16,12 +16,15 @@ import { type Answer, consoleAnswers, type FormId, type PlanFormId, type PlanOnl
|
|||
import { APP_CONTENT_CARDS, playOnlyPart, startLabel } from './console-play-plans';
|
||||
import { APPLE_PRIVACY_TYPES } from './app-privacy';
|
||||
import { typeLabels } from './console-forms';
|
||||
import { playAppIdInText } from './play-app-id';
|
||||
import { APPLE_PURPOSES } from './console-answers';
|
||||
|
||||
export type RowStatus = 'match' | 'mismatch' | 'not-found' | 'ask-owner';
|
||||
export interface VerifyRow { item: string; expected: string; seen: string; status: RowStatus; note?: string }
|
||||
export interface VerifyResult {
|
||||
form: PlanFormId;
|
||||
/** play-app-record: the Play Console app id read from the URL after Create app (/app/<appId>/...). */
|
||||
playAppId?: string;
|
||||
source: 'page-text' | 'inputs' | 'api';
|
||||
rows: VerifyRow[];
|
||||
mismatches: number;
|
||||
|
|
@ -143,6 +146,8 @@ export interface InputStates {
|
|||
checked?: string[];
|
||||
/** The visible page text (for "Select …" hints and availability messages). */
|
||||
text?: string;
|
||||
/** location.href (after Create app: /app/<appId>/app-dashboard). */
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/** The capture snippet an agent runs in the page for `console verify play-app-record --from-text -` (prints JSON). */
|
||||
|
|
@ -150,7 +155,7 @@ export const PLAY_CREATE_CAPTURE = `(() => { const t = e => (e?.innerText ?? e?.
|
|||
const name = e => e.getAttribute('aria-label') || t(e.labels?.[0]) || (e.getAttribute('aria-labelledby') || '').split(/\\s+/).map(id => t(document.getElementById(id))).join(' ').trim() || t(e.closest('label')) || t(e.parentElement);
|
||||
const fields = {}; for (const i of document.querySelectorAll('input:not([type]), input[type=text], textarea')) if (i.offsetParent) fields[name(i)] = i.value;
|
||||
const checked = [...document.querySelectorAll('[role=radio][aria-checked=true], input[type=radio]:checked')].map(r => name(r) || t(r.closest('material-radio')));
|
||||
return JSON.stringify({ fields, checked, text: (document.querySelector('main') ?? document.body).innerText }); })()`;
|
||||
return JSON.stringify({ url: location.href, fields, checked, text: (document.querySelector('main') ?? document.body).innerText }); })()`;
|
||||
|
||||
export function parseInputStates(raw: string): InputStates | null {
|
||||
const s = raw.trim();
|
||||
|
|
@ -159,7 +164,7 @@ export function parseInputStates(raw: string): InputStates | null {
|
|||
const j = JSON.parse(s) as Record<string, unknown>;
|
||||
const fields = j.fields && typeof j.fields === 'object' ? Object.fromEntries(Object.entries(j.fields as Record<string, unknown>).filter(([, v]) => typeof v === 'string')) as Record<string, string> : undefined;
|
||||
const checked = Array.isArray(j.checked) ? j.checked.filter((x): x is string => typeof x === 'string') : undefined;
|
||||
return { ...(fields ? { fields } : {}), ...(checked ? { checked } : {}), ...(typeof j.text === 'string' ? { text: j.text } : {}) };
|
||||
return { ...(fields ? { fields } : {}), ...(checked ? { checked } : {}), ...(typeof j.text === 'string' ? { text: j.text } : {}), ...(typeof j.url === 'string' ? { url: j.url } : {}) };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +174,16 @@ export const SELECT_HINT = /^select (app or game|free or paid|an? .+)$/i;
|
|||
export function verifyPlayAppRecord(config: ShipConfig, raw: string): VerifyResult {
|
||||
const answers = consoleAnswers('play-app-record', config).filter(a => !a.agreement);
|
||||
const st = parseInputStates(raw);
|
||||
// after Create app (verified 2026-10-12 / 2026-10-13): the console redirects to /app/<appId>/app-dashboard; the form
|
||||
// is gone, so only the app id is checked (and handed to the play.appId upsert)
|
||||
const appId = playAppIdInText(st?.url ?? raw);
|
||||
const formShown = st ? Object.keys(st.fields ?? {}).length > 0 : /\b(App name|App package name|Uygulama adı)\b/.test(raw);
|
||||
if (appId && !formShown) {
|
||||
const want = config.play?.appId;
|
||||
const row: VerifyRow = { item: 'Play app id (the URL after Create app)', expected: want ?? '(not in the config yet: play.appId)', seen: appId,
|
||||
status: want && want !== appId ? 'mismatch' : 'match', ...(want && want !== appId ? { note: 'play.appId differs: --write replaces it' } : {}) };
|
||||
return { ...result('play-app-record', st ? 'inputs' : 'page-text', [row], ['The Create app form is gone: the app was created. --write saves the id into play.appId.']), playAppId: appId };
|
||||
}
|
||||
const text = st?.text ?? (st ? '' : raw);
|
||||
const ls = lines(text);
|
||||
const rows: VerifyRow[] = [];
|
||||
|
|
@ -332,7 +347,7 @@ export function verifyPlayDeclaration(form: Exclude<PlanOnlyFormId, 'play-app-co
|
|||
const blob = fold(text);
|
||||
const rows: VerifyRow[] = [];
|
||||
for (const a of part.answers) {
|
||||
if (a.status === 'agreement') continue;
|
||||
if (a.status === 'agreement' || a.id === 'credentials') continue; // the demo account is never read back or printed
|
||||
if (a.status === 'ask-owner' || a.value === null) { rows.push({ item: a.question, expected: '(owner decides)', seen: '-', status: 'ask-owner' }); continue; }
|
||||
const want = a.action === 'type' || a.action === 'choose' ? String(a.value) : a.label ?? '';
|
||||
const plain = !want || /^(yes|no)$/i.test(want) || a.action === 'untick';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue