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.
77 lines
5.8 KiB
HTML
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>
|