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
|
|
@ -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;
|
||||
/** An agreement the page's Next waits for (the IARC Terms of Use, verified 2026-10-13): the flow waits until the owner
|
||||
* ticked it instead of pressing a Next that does nothing. */
|
||||
blocksNext?: 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;
|
||||
|
|
@ -157,7 +160,7 @@ 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] },
|
||||
{ id: 'agree:iarcTerms', kind: 'checkbox', agreement: true, blocksNext: 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
|
||||
|
|
@ -191,9 +194,42 @@ const FORBIDDEN = new RegExp(`(?<![${L}])(?:(?:${EN.map(esc).join('|')})(?![${L}
|
|||
/** Controls never pressed by storebound or by an agent following `console plan`, whatever the form: help panels and
|
||||
* the final buttons (the agent-facing never-press list is built from this and FORM_BUTTONS). */
|
||||
export const NEVER_PRESS = ['Publish', 'Submit', 'Create', 'Create app', 'Send', 'Release', 'Definitions and Examples', '?', 'More information'];
|
||||
/** Play Console's publishing buttons (Publishing overview, verified 2026-10-12): in every Play plan's never-press list. */
|
||||
export const PLAY_NEVER_PRESS = ['Send changes for review', 'Submit N changes for review', 'Restart review'];
|
||||
|
||||
/** true when storebound must not click a control with this accessible name. */
|
||||
export const forbiddenControl = (name: string): boolean => FORBIDDEN.test(name.toLocaleLowerCase('tr').normalize('NFC')) || FORBIDDEN.test(name.toLowerCase());
|
||||
/** true when storebound must not click a control with this accessible name (field clicks: absolute; the scoped
|
||||
* exceptions are navAllowed, createAppAllowed, setUpAllowed, flowAllowed and declarationSaveAllowed). */
|
||||
export const forbiddenControl = (name: string): boolean => FORBIDDEN.test(name.toLocaleLowerCase('tr').normalize('NFC')) || FORBIDDEN.test(name.toLowerCase())
|
||||
|| publishingControl(name);
|
||||
|
||||
/**
|
||||
* Buttons that send something to Google or Apple, publish, or restart a review: never pressed by storebound or an agent,
|
||||
* in any form and by any exception (English and Turkish). Play's "Send changes for review", "Submit N changes for
|
||||
* review" and "Restart review" (Publishing overview; verified 2026-10-12), Submit, Publish, a release or rollout.
|
||||
*/
|
||||
const PUBLISHING = /(?<![\p{L}])(?:(?:send (?:\d+ )?changes? for review|submit|publish|restart (?:the |your )?review|send for review|release|roll ?out)(?![\p{L}])|yayınla|yayımla|gönder|incelemeye|incelemeyi yeniden)/iu;
|
||||
export const publishingControl = (name: string): boolean => PUBLISHING.test(name.normalize('NFC').replace(/\s+/g, ' '));
|
||||
|
||||
/** The exact names of a Play App content card's Save (English and Turkish). */
|
||||
const CARD_SAVE = ['save', 'kaydet'];
|
||||
/** Where a card Save sits: the page URL (an App content card is /app/<appId>/app-content/<card>, not the overview). */
|
||||
export interface CardWhere { url: string }
|
||||
/**
|
||||
* The App content declaration Save (verified 2026-10-13): saving a card (Privacy policy, Ads, Sign in details,
|
||||
* Advertising ID, Government apps, Financial features, Health apps, Target audience, the content rating questionnaire
|
||||
* and its Summary, Data safety) is not public: Play sends it to Google only with the next "Send changes for review" in
|
||||
* Publishing overview, which stays the owner's. So the agent (and the scripted engine) presses it: a button whose whole
|
||||
* name is exactly Save / Kaydet on a Play Console App content card page. Everything that publishes or sends
|
||||
* (publishingControl) stays refused here too.
|
||||
*/
|
||||
export function declarationSaveAllowed(name: string, role: string, where: CardWhere): boolean {
|
||||
const n = name.trim().replace(/\s+/g, ' ').toLocaleLowerCase('tr');
|
||||
if (role !== 'button' || !CARD_SAVE.includes(n) || publishingControl(name)) return false;
|
||||
try {
|
||||
const u = new URL(where.url);
|
||||
return /\/developers\/\d{1,25}\/app\/\d{1,25}\/app-content\/(?!overview(?:\/|$))[a-z-]+/.test(u.pathname);
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticking a checkbox / radio never saves or sends anything, so the button word list above does not apply to them (their
|
||||
|
|
@ -395,7 +431,9 @@ export function navAllowed(name: string, role: string): boolean {
|
|||
// Google "Target audience and content" https://support.google.com/googleplay/android-developer/answer/9867159 ;
|
||||
// Google "Create and set up your app" https://support.google.com/googleplay/android-developer/answer/9859152
|
||||
|
||||
export type ButtonClass = 'navigation' | 'draft-save' | 'final-submit';
|
||||
// A fourth class (2026-10-13): declaration-save, an App content card's Save. It stores a declaration that Google gets
|
||||
// only with the next "Send changes for review" (the owner's, in Publishing overview), so the agent presses it.
|
||||
export type ButtonClass = 'navigation' | 'draft-save' | 'declaration-save' | 'final-submit';
|
||||
export interface ButtonRule { button: string; class: ButtonClass; where: string; auto: boolean }
|
||||
|
||||
const NEXT: Name[] = ['Next', 'Continue', 'İleri', 'Sonraki', 'Devam', 'Devam et'];
|
||||
|
|
@ -448,13 +486,15 @@ export const FORM_BUTTONS: Record<FormId, ButtonRule[]> = {
|
|||
{ 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 / 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 },
|
||||
{ button: 'IARC Terms of Use box', class: 'final-submit', where: 'page 1: an agreement, never ticked by the agent or storebound (the owner ticks it before the run: Next does nothing until it is ticked, verified 2026-10-13)', auto: false },
|
||||
{ button: 'Save (Summary page)', class: 'declaration-save', where: 'Summary (per-authority ratings; "Save", not "Submit", verified 2026-10-12): stores the rating declaration; Google gets it only with the next Send changes for review', auto: true },
|
||||
{ button: 'Send changes for review', class: 'final-submit', where: 'Publishing overview, after every App content card: sends the changes to Google (the owner\'s)', auto: false },
|
||||
],
|
||||
'play-target-audience': [
|
||||
{ button: 'Start / Manage', class: 'navigation', where: 'App content: opens the section', auto: true },
|
||||
{ button: 'Next', class: 'navigation', where: 'between Target age, App details, Ads and Store presence', auto: true },
|
||||
{ button: 'Save', class: 'final-submit', where: 'the last page: stores the declaration, which Google then reviews', auto: false },
|
||||
{ button: 'Save', class: 'declaration-save', where: 'the last page: stores the declaration; Google gets it only with the next Send changes for review', auto: true },
|
||||
{ button: 'Send changes for review', class: 'final-submit', where: 'Publishing overview, after every App content card: sends the changes to Google (the owner\'s)', auto: false },
|
||||
],
|
||||
};
|
||||
|
||||
|
|
@ -491,10 +531,14 @@ export function flowClass(form: FormId, name: string, role: string, where: FlowW
|
|||
return null;
|
||||
}
|
||||
if (nameIs(n, SAVE_DRAFT)) return 'draft-save';
|
||||
if (nameIs(n, SAVE) && DECLARATION_SAVE_FORMS.includes(form)) return 'declaration-save';
|
||||
if (nameIs(n, SAVE) || nameIs(n, DONE)) return 'final-submit';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The scripted forms whose Save is an App content declaration save (the agent's: verified 2026-10-13). */
|
||||
export const DECLARATION_SAVE_FORMS: FormId[] = ['play-content-rating', 'play-target-audience'];
|
||||
|
||||
/** 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`]);
|
||||
|
||||
|
|
@ -514,13 +558,17 @@ export function setUpAllowed(text: string, where: FlowWhere): boolean {
|
|||
export const AUTO_DRAFT_SAVE: FormId[] = ['apple-app-privacy', 'play-content-rating'];
|
||||
|
||||
/**
|
||||
* The flow click gate: a navigation control of the form, App Privacy's Save inside a questionnaire dialog, or the
|
||||
* content rating's Save draft (drafts: nothing is public or sent before Publish / Submit). Everything else (Publish,
|
||||
* Submit, Create, a final Save, an unknown control) stays the owner's click.
|
||||
* The flow click gate: a navigation control of the form, App Privacy's Save inside a questionnaire dialog, the
|
||||
* content rating's Save draft (drafts: nothing is public or sent before Publish / Submit), or a Play App content
|
||||
* declaration's Save (content rating Summary, target audience: sent to Google only with the owner's Send changes for
|
||||
* review). Everything else (Publish, Submit, Create, Send changes for review, Restart review, an unknown control)
|
||||
* stays the owner's click.
|
||||
*/
|
||||
export function flowAllowed(form: FormId, name: string, role: string, where: FlowWhere): boolean {
|
||||
const c = flowClass(form, name, role, where);
|
||||
if (publishingControl(name)) return false;
|
||||
if (c === 'navigation') return true;
|
||||
if (c === 'declaration-save') return DECLARATION_SAVE_FORMS.includes(form) && !where.inDialog;
|
||||
if (c !== 'draft-save' || !AUTO_DRAFT_SAVE.includes(form)) return false;
|
||||
return form === 'apple-app-privacy' ? where.inDialog && where.privacyPage : true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue