console fill apple-app-privacy: set up each data type from the real page ('Set Up <type>' is a plain text, clicked by its exact text in the Data Types section), fill its dialog step by step, dump and pause on an unknown step; '?' and Publish never clicked

This commit is contained in:
CRTkafa 2026-10-12 07:36:06 +08:00
parent 3aedf9e137
commit e0bef82b06
7 changed files with 500 additions and 28 deletions

View file

@ -63,6 +63,8 @@ const IARC_NAMES: Record<string, Name[]> = {
* read from a real console in October 2026): "Purchases" for Purchase History, "Other Data" for Other Data Types.
*/
export const TYPE_ALIASES: Record<string, string[]> = { 'Purchase History': ['Purchases'], 'Other Data Types': ['Other Data'] };
/** A data type's name and the console's other labels for it ("Purchase History", "Purchases"). */
export const typeLabels = (t: string): string[] => [t, ...(TYPE_ALIASES[t] ?? [])];
/**
* Candidate names of a checkbox labelled `label`: the exact name, then the name that STARTS with the label and goes on
@ -103,12 +105,13 @@ export function formDef(id: FormId, answers: Answer[]): FormDef {
] }];
const types = answers.filter(a => a.id.startsWith('type:'));
if (types.length) sections.push({ title: 'Data types', fields: types.map(a => ({ id: a.id, kind: 'checkbox', names: labelNames(a.id.slice(5), TYPE_ALIASES[a.id.slice(5)]) })) });
// one data type's dialog: purposes, linked to the user, tracking; one page each (Next between, Save at the end)
// or all on one page: each is its own section, the later two are `page`s
// one data type's dialog (opened by "Set Up <type>" in the Data Types section): purposes, linked to the user,
// tracking; one step each (Next between, Save at the end) or all on one page. Every part is a `page`: a dialog step
// with none of its fields is an unknown step (the watchdog dumps it and pauses), never a reason to stop the run
for (const a of types.filter(t => t.value === true && has(`linked:${t.id.slice(5)}`))) {
const n = a.id.slice(5);
const dialog = [n, new RegExp(`^${esc(n)}\\b`, 'i')];
sections.push({ title: `${n}: purposes`, dialog, type: n, fields: APPLE_PURPOSES.map(p => ({ id: `purpose:${n}:${p}`, kind: 'checkbox' as const, names: labelNames(p) })) });
const dialog = typeLabels(n).flatMap(l => [l, new RegExp(`^${esc(l)}\\b`, 'i')]);
sections.push({ title: `${n}: purposes`, dialog, type: n, page: true, fields: APPLE_PURPOSES.map(p => ({ id: `purpose:${n}:${p}`, kind: 'checkbox' as const, names: labelNames(p) })) });
sections.push({ title: `${n}: linked to the user`, dialog, type: n, page: true, fields: [{ id: `linked:${n}`, kind: 'radio', options: yesNo([/^Yes,.*\blinked to the user/i], [/^No,.*\blinked to the user/i]) }] });
sections.push({ title: `${n}: tracking`, dialog, type: n, page: true, fields: [{ id: `tracking:${n}`, kind: 'radio', options: yesNo([/^Yes,.*\btracking/i], [/^No,.*\btracking/i]) }] });
}
@ -364,7 +367,9 @@ export const FORM_BUTTONS: Record<FormId, ButtonRule[]> = {
'apple-app-privacy': [
{ button: 'Get Started', class: 'navigation', where: 'App Privacy page, first time: opens the questionnaire', auto: true },
{ button: 'Edit (next to Data Types)', class: 'navigation', where: 'Data Types section: opens the data type list', auto: true },
{ button: '<data type> / Set Up <data type>', class: 'navigation', where: 'Data Types section: opens that type\'s questions', auto: true },
{ button: 'Set Up <data type>', class: 'navigation', where: 'Data Types section, one per collected type (a plain text with a click handler, not a button): opens that type\'s questions', auto: true },
{ button: 'Edit (next to one data type)', class: 'navigation', where: 'Data Types section, a type set up before: reopens its questions, only when its purposes differ from the config', auto: true },
{ button: '? / More information', class: 'navigation', where: 'next to each heading and data type: help only (never needed)', auto: false },
{ button: 'Next / Continue', class: 'navigation', where: 'questionnaire dialogs', auto: true },
{ button: 'Back', class: 'navigation', where: 'questionnaire dialogs (never needed)', auto: false },
{ button: 'Save', class: 'draft-save', where: 'questionnaire dialogs: stores the answers, not public until Publish', auto: true },
@ -434,6 +439,21 @@ export function flowClass(form: FormId, name: string, role: string, where: FlowW
return null;
}
/** The texts that open one data type's questions ("Set Up Coarse Location"; Turkish "<type> ayarla"), for its labels. */
export const setUpTexts = (t: string): string[] => typeLabels(t).flatMap(l => [`Set Up ${l}`, `${l} ayarla`]);
/**
* The Set Up click gate: App Store Connect shows "Set Up <type>" as a plain text (a <p> in a div with a click handler,
* no button or link role), so the role-based gate cannot see it. It is clicked only when its whole text is exactly
* "Set Up " + one of the data types storebound answers (or their console labels), on the App Privacy page, in the Data
* Types section and outside any dialog. Nothing else is clicked by text (the "?" help buttons, Publish never).
*/
export function setUpAllowed(text: string, where: FlowWhere): boolean {
const n = norm(text).toLocaleLowerCase('tr');
if (!where.privacyPage || !where.inDataTypes || where.inDialog || forbiddenControl(n)) return false;
return where.dataTypes.some(t => setUpTexts(t).some(x => x.toLocaleLowerCase('tr') === n));
}
/** The forms whose draft save storebound clicks (the owner agreed): nothing there is public or sent before the final button. */
export const AUTO_DRAFT_SAVE: FormId[] = ['apple-app-privacy', 'play-content-rating'];