storebound/tests/fixtures/console/apple-app-privacy-flow.html
CRT 8ffc83bb39 console fill: walk the whole form; the owner only reviews and presses the final button
App Privacy stopped at "now click Get Started yourself" and every page
asked the owner to click Next. The owner should not click what
storebound can safely do.

- Every button of every console form is classified (navigation,
  draft-save, final-submit) in FORM_BUTTONS (cli/lib/console-forms.ts),
  citing Apple's and Google's help pages. A scoped flow gate
  (flowAllowed) lets navigation through; the generic click gate stays
  absolute for field clicks.
- App Privacy: Get Started, Edit next to Data Types (never the Privacy
  Policy Edit), each data type / "Set Up <type>", Next, and the Save
  inside the questionnaire dialogs (a draft: Apple publishes only after
  Publish, per "Manage app privacy"). Each type's dialog may be one page
  or purposes > Next > linked > Next > tracking > Save. It ends with
  "review the summary, then press Publish yourself". Publish is never
  clicked.
- Play content rating and target audience: Next by itself. Save draft,
  Submit, the final Save, Create and Create app stay the owner's.
- An uncertain answer stops the walk on its page (outlined, "answer
  needed on ...") until the owner has set it; then it goes on.

Fixture apple-app-privacy-flow.html (first time, ?existing=1, decoys);
tests/console-flow.test.ts with the guardrail that no log line asks the
owner to click a navigation control.
2026-10-12 06:51:01 +08:00

77 lines
5.8 KiB
HTML

<!doctype html>
<!-- MODELED, not recorded: App Store Connect > App Privacy as a whole questionnaire, written from Apple's help page
"Manage app privacy" (Get Started; "Yes, we collect data" > Next > data types > Save; each data type in the Data
Types section opens its questions: purposes > Next > linked to the user > Next > tracking > Save; Edit next to Data
Types changes the list; Publish at the top right publishes, with a confirmation). ?existing=1: answers exist (no
Get Started; Edit next to Data Types). Decoys: Publish, See Details, and the Edit next to the Privacy Policy URL.
window.__saves counts the draft saves. Placeholder data only. -->
<html lang="en"><head><meta charset="utf-8"><title>App Privacy - App Store Connect</title><script src="/_common.js"></script></head>
<body>
<h1>App Privacy</h1>
<button type="button" id="publish">Publish</button>
<section><h2>Privacy Policy</h2><p>https://example.com/privacy</p><button type="button">Edit</button></section>
<p id="intro"><button type="button" id="start">Get Started</button></p>
<section id="dt" hidden><h2>Data Types</h2><button type="button" id="edit">Edit</button><ul id="list"></ul></section>
<section><h2>Product Page Preview</h2><button type="button">See Details</button></section>
<div role="dialog" aria-label="Data Collection" id="collect" hidden>
<div id="c1">
<label><input type="radio" name="c" value="yes"> Yes, we collect data from this app</label>
<label><input type="radio" name="c" value="no"> No, we do not collect data from this app</label>
<button type="button">Cancel</button> <button type="button" id="c1next">Next</button> <button type="button" id="c1save" hidden>Save</button>
</div>
<div id="c2" hidden><div id="types"></div><button type="button" id="c2back">Back</button> <button type="button" id="c2save">Save</button></div>
</div>
<div id="per"></div>
<script>
const TYPES = ['Name', 'Email Address', 'Phone Number', 'Physical Address', 'Other User Contact Info', 'Health', 'Fitness', 'Payment Info', 'Credit Info',
'Other Financial Info', 'Precise Location', 'Coarse Location', 'Sensitive Info', 'Contacts', 'Emails or Text Messages', 'Photos or Videos', 'Audio Data',
'Gameplay Content', 'Customer Support', 'Other User Content', 'Browsing History', 'Search History', 'User ID', 'Device ID', 'Purchase History',
'Product Interaction', 'Advertising Data', 'Other Usage Data', 'Crash Data', 'Performance Data', 'Other Diagnostic Data', 'Environment Scanning', 'Hands',
'Head', 'Other Data Types'];
const PURPOSES = ['Third-Party Advertising', "Developer's Advertising or Marketing", 'Analytics', 'Product Personalization', 'App Functionality', 'Other Purposes'];
const $ = id => document.getElementById(id);
window.__saves = 0;
for (const t of TYPES) { const l = document.createElement('label'); l.innerHTML = '<input type="checkbox"> '; l.append(t); $('types').append(l); }
const existing = new URLSearchParams(location.search).has('existing');
const show = (el, on) => { el.hidden = !on; };
function listTypes(names) {
$('list').innerHTML = '';
for (const [i, t] of names.entries()) {
const li = document.createElement('li');
// one data type opens with a "Set Up <type>" button, the next with a link named like the type
li.innerHTML = i % 2 === 0 ? `<button type="button">Set Up ${t}</button>` : `<a href="#">${t}</a>`;
li.querySelector('button, a').addEventListener('click', e => { e.preventDefault(); openType(t); });
$('list').append(li);
}
show($('dt'), true); show($('intro'), false);
}
// the per-type dialog: three pages (purposes > Next > linked > Next > tracking > Save)
function openType(t) {
const d = document.createElement('div'); d.setAttribute('role', 'dialog'); d.setAttribute('aria-label', t);
d.innerHTML = `<div data-p="1">${PURPOSES.map(p => `<label><input type="checkbox"> ${p}</label>`).join('')}<button type="button" data-go="2">Next</button></div>`
+ `<div data-p="2" hidden><label><input type="radio" name="l"> Yes, ${t} collected from this app is linked to the user's identity</label>`
+ `<label><input type="radio" name="l"> No, ${t} collected from this app is not linked to the user's identity</label><button type="button" data-go="1b">Back</button> <button type="button" data-go="3">Next</button></div>`
+ `<div data-p="3" hidden><label><input type="radio" name="t"> Yes, we use ${t} for tracking purposes</label>`
+ `<label><input type="radio" name="t"> No, we do not use ${t} for tracking purposes</label><button type="button" data-save>Save</button></div>`;
d.addEventListener('click', e => {
const go = e.target.closest('[data-go]')?.getAttribute('data-go');
if (go && go !== '1b') d.querySelectorAll('[data-p]').forEach(p => { p.hidden = p.getAttribute('data-p') !== go; });
if (e.target.closest('[data-save]')) { window.__saves++; d.remove(); }
});
$('per').append(d);
}
if (existing) listTypes(['Device ID', 'Crash Data']);
$('start').addEventListener('click', () => { show($('collect'), true); show($('c1'), true); show($('c2'), false); });
$('edit').addEventListener('click', () => { show($('collect'), true); show($('c1'), false); show($('c2'), true); });
// "No" saves at once; "Yes" goes on with Next
document.querySelectorAll('input[name=c]').forEach(r => r.addEventListener('change', () => { const no = r.value === 'no' && r.checked; show($('c1next'), !no); show($('c1save'), no); }));
$('c1next').addEventListener('click', () => { show($('c1'), false); show($('c2'), true); });
$('c1save').addEventListener('click', () => { window.__saves++; show($('collect'), false); });
$('c2save').addEventListener('click', () => {
window.__saves++;
const picked = [...$('types').querySelectorAll('label')].filter(l => l.querySelector('input').checked).map(l => l.textContent.trim());
show($('collect'), false); listTypes(picked);
});
</script>
</body></html>