Play console steps without an API as agent plans: play-store-settings (App category; play listing cannot set it), play-app-content (every card in order, one owner batch) and its cards (privacy policy, ads, sign in details, advertising ID, government, financial, health), play-production-countries and play-production-release; IARC plan with the Terms of Use agreement, follow-ups (console.contentRating.details, products[].randomRewards, social) and the Summary's final Save; config play.category / countries / managedPublishing, console.appContent; check, validate, play status and play release reminders; inPageLoop helper and material checkbox clicks; App Privacy published signal; first IAPs via apple iap-submit --with-version. Pitfalls 27, 28
This commit is contained in:
parent
dbd9f2973c
commit
b7bfe89151
37 changed files with 1624 additions and 121 deletions
|
|
@ -6,8 +6,8 @@
|
|||
// The names are modeled on Apple's and Google's public help pages (sources in console-answers.ts), not recorded from a
|
||||
// signed-in console: the exact App Privacy dialog titles, the IARC question wording and the Play category names could
|
||||
// not be verified without an account, which is why every field has candidates and the engine falls back on a miss.
|
||||
import { AGE_GROUPS, appleName, type ShipConfig } from './config';
|
||||
import { AGE_LABELS, type Answer, APPLE_PURPOSES, type FormId } from './console-answers';
|
||||
import { AGE_GROUPS, appleName, IARC_DETAIL_SECTIONS, type ShipConfig } from './config';
|
||||
import { AGE_LABELS, type Answer, APPLE_PURPOSES, type FormId, iarcSectionName } from './console-answers';
|
||||
|
||||
/** An accessible name: a string matches exactly, a RegExp matches the name. */
|
||||
export type Name = string | RegExp;
|
||||
|
|
@ -22,6 +22,9 @@ export interface FieldDef {
|
|||
options?: Record<string, Name[]>;
|
||||
/** An agreement checkbox: located and highlighted, NEVER ticked (whatever the answer says). */
|
||||
agreement?: boolean;
|
||||
/** radio: the options count only inside the named group (no group on the page: the field is not there). The IARC
|
||||
* follow-ups, whose Yes / No options would otherwise match every question of the page. */
|
||||
scoped?: boolean;
|
||||
}
|
||||
|
||||
export interface SectionDef {
|
||||
|
|
@ -33,9 +36,37 @@ export interface SectionDef {
|
|||
page?: boolean;
|
||||
/** App Privacy: the data type this section belongs to (its dialog opens from the Data Types section). */
|
||||
type?: string;
|
||||
/** Shown only after an answer reveals it (the IARC follow-ups): never needed to finish the form. */
|
||||
optional?: boolean;
|
||||
fields: FieldDef[];
|
||||
}
|
||||
|
||||
/** Candidate group names of the IARC yes / no follow-ups (modeled from the section structure recorded 2026-10-12). */
|
||||
const DETAIL_NAMES: Record<string, Name[]> = {
|
||||
childlike: [/(cartoon|childlike|child-like)/i], historicalWar: [/historical|\bwar\b/i], innocents: [/innocent|defen[cs]eless/i], fierceSounds: [/sound/i],
|
||||
playerTrading: [/trad(e|ing)/i], block: [/\bblock/i], report: [/\breport/i], moderation: [/moderat/i], friendsOnly: [/friend/i],
|
||||
naziSymbols: [/nazi/i], koreaHistory: [/korea/i], terrorism: [/terror/i], crimes: [/crime/i], setting: [/setting/i], blood: [/blood/i],
|
||||
};
|
||||
const NEVER: Name[] = [/^\u0000$/];
|
||||
|
||||
/** The field of one IARC follow-up answer (d:<section>:<key>[:<label>] or d:<section>:other:<question>[:<label>]). */
|
||||
function detailField(a: Answer): FieldDef {
|
||||
const [, , key = '', ...rest] = a.id.split(':');
|
||||
const tail = rest.join(':');
|
||||
if (key === 'humans') return { id: a.id, kind: 'checkbox', names: [/^(violence )?(against )?humans?\b/i] };
|
||||
if (key === 'nonHumans') return { id: a.id, kind: 'checkbox', names: [/non-?humans?/i] };
|
||||
if (key === 'followUps' || ((key === 'reactions' || key === 'presentation') && !tail)) return { id: a.id, kind: 'checkbox', names: NEVER };
|
||||
if (key === 'reactions' || key === 'presentation') return { id: a.id, kind: 'checkbox', names: labelNames(tail) };
|
||||
if (key === 'other') {
|
||||
const [q = '', ...l] = rest, label = l.join(':');
|
||||
if (label) return { id: a.id, kind: 'checkbox', names: labelNames(label) };
|
||||
const group: Name[] = [new RegExp(`^${esc(q)}`, 'i')];
|
||||
return typeof a.value === 'string' ? { id: a.id, kind: 'radio', scoped: true, names: group, options: { [a.value]: labelNames(a.value) } } : { id: a.id, kind: 'radio', scoped: true, names: group, options: yesNo() };
|
||||
}
|
||||
if (key === 'setting' || key === 'blood') return { id: a.id, kind: 'radio', scoped: true, names: DETAIL_NAMES[key], options: typeof a.value === 'string' ? { [a.value]: labelNames(a.value) } : { unknown: NEVER } };
|
||||
return { id: a.id, kind: 'radio', scoped: true, names: DETAIL_NAMES[key] ?? NEVER, options: yesNo() };
|
||||
}
|
||||
|
||||
export interface FormDef { id: FormId; sections: SectionDef[] }
|
||||
|
||||
// Console labels use typographic quotes ("Developer’s Advertising", "the user’s identity"): every pattern built from a
|
||||
|
|
@ -126,8 +157,14 @@ export function formDef(id: FormId, answers: Answer[]): FormDef {
|
|||
{ title: 'Email address and category', fields: [
|
||||
{ id: 'email', kind: 'text', names: ['Email address', 'Email'] },
|
||||
{ id: 'category', kind: 'radio', options: { 'Game': [/^Game\b/i], 'Social or communication': [/^Social/i], 'All other app types': [/^All other app types/i, /^Utility/i] } },
|
||||
{ id: 'agree:iarcTerms', kind: 'checkbox', agreement: true, names: [/IARC Terms of Use/i, /terms of use/i] },
|
||||
] },
|
||||
{ title: 'Questionnaire', fields: answers.filter(a => a.id.startsWith('q:')).map(a => ({ id: a.id, kind: 'radio' as const, names: IARC_NAMES[a.id.slice(2)], options: yesNo() })) },
|
||||
// follow-ups a Yes reveals on the same page (and Miscellaneous): optional, filled when they appear
|
||||
...IARC_DETAIL_SECTIONS.flatMap(sec => {
|
||||
const fields = answers.filter(a => a.id.startsWith(`d:${sec}:`)).map(a => detailField(a));
|
||||
return fields.length ? [{ title: `Questionnaire: ${iarcSectionName(sec)} follow-ups`, page: true, optional: true, fields }] : [];
|
||||
}),
|
||||
] };
|
||||
case 'play-target-audience': return { id, sections: [
|
||||
{ title: 'Target age', fields: AGE_GROUPS.map(g => ({ id: `age:${g}`, kind: 'checkbox' as const, names: [AGE_LABELS[g], AGE_LABELS[g].replace(/^Ages /, '')] })) },
|
||||
|
|
@ -163,7 +200,7 @@ export const forbiddenControl = (name: string): boolean => FORBIDDEN.test(name.t
|
|||
* names carry help text: "Product InteractionSuch as ... saved place in a game"). What a tick must never do is accept an
|
||||
* agreement: those are refused here.
|
||||
*/
|
||||
const AGREEMENT = /\b(?:i agree|agree to|accept(?:s|ed)?\b|terms of service|developer program polic|export laws?|declaration|consent)|kabul|onaylıyorum|beyan/iu;
|
||||
const AGREEMENT = /\b(?:i agree|agree to|accept(?:s|ed)?\b|terms of service|terms of use|developer program polic|export laws?|declaration|consent)|kabul|onaylıyorum|beyan|kullanım koşulları/iu;
|
||||
export const forbiddenTick = (name: string): boolean => AGREEMENT.test(name.toLocaleLowerCase('tr').normalize('NFC'));
|
||||
|
||||
const SIGN_IN_HOSTS = ['appleid.apple.com', 'idmsa.apple.com', 'accounts.google.com'];
|
||||
|
|
@ -408,10 +445,11 @@ export const FORM_BUTTONS: Record<FormId, ButtonRule[]> = {
|
|||
{ button: 'Create app (form)', class: 'final-submit', where: 'the Create app form: creates the app', auto: false },
|
||||
],
|
||||
'play-content-rating': [
|
||||
{ button: 'Start / Start new questionnaire', class: 'navigation', where: 'App content: opens the questionnaire', auto: true },
|
||||
{ button: 'Start / Start questionnaire', class: 'navigation', where: 'App content > Content ratings (content-rating-overview): opens the questionnaire', auto: true },
|
||||
{ button: 'Next', class: 'navigation', where: 'after the category, and from the questions to the Summary', auto: true },
|
||||
{ button: 'Save draft', class: 'draft-save', where: 'questionnaire: stores the answers as the one draft of the app; nothing goes to IARC before Submit', auto: true },
|
||||
{ button: 'Submit', class: 'final-submit', where: 'Summary: sends the questionnaire to IARC', auto: false },
|
||||
{ button: 'Save draft / Save (questionnaire page)', class: 'draft-save', where: 'questionnaire: stores the answers as the one draft of the app; nothing goes to IARC before the Summary\'s Save', auto: true },
|
||||
{ button: 'IARC Terms of Use box', class: 'final-submit', where: 'page 1: an agreement, never ticked by the agent or storebound', auto: false },
|
||||
{ button: 'Save (Summary page)', class: 'final-submit', where: 'Summary (per-authority ratings): sends the questionnaire to IARC and applies the ratings (verified 2026-10-12: "Save", not "Submit")', auto: false },
|
||||
],
|
||||
'play-target-audience': [
|
||||
{ button: 'Start / Manage', class: 'navigation', where: 'App content: opens the section', auto: true },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue