105 lines
7.7 KiB
HTML
105 lines
7.7 KiB
HTML
<!doctype html>
|
|
<!-- App Store Connect > App Privacy after the data types were saved. The PAGE mirrors the STRUCTURE of a real console
|
|
page (sanitized debug dump, October 2026; structure only, no names, ids or data copied): an <h2> "App Privacy" with
|
|
Publish next to it; an <h3> "Privacy Policy" with its Edit and a "?" button; an <h3> "Data Types" in its own box with
|
|
its Edit and a one-line summary; then, in the NEXT box (not under the heading's box), the category names and one
|
|
row per data type as plain <p> texts: the type label, a "?" help button (aria-label "More information") and
|
|
"Set Up <type>", a <p> inside a div with the click handler (no button role, no link). Purchase History's row has
|
|
only its "Set Up" (the console shows the category "Purchases" above it).
|
|
The per-type DIALOG is MODELED from Apple's help page "Manage app privacy"
|
|
(https://developer.apple.com/help/app-store-connect/manage-app-information/manage-app-privacy: for each data type,
|
|
the purposes, whether it is linked to the user's identity, whether it is used for tracking; Next between, Save at the
|
|
end) in the markup of the real Data Collection dialog: <dialog role="dialog"> titled by <h2><span>, checkboxes whose
|
|
<label> holds the label and its help text as two <p>, radios "<strong>Yes</strong>, ..." / "<strong>No</strong>, ...",
|
|
Back / Cancel / Next / Save. Saving shows the type's purposes in its row and an Edit instead of "Set Up".
|
|
?unknown=<type>: that type's second step is one storebound does not know (switches). ?preset=<type>:<purpose>|...
|
|
(repeatable): that type was set up before with these purposes.
|
|
window.__setups: the "Set Up" texts clicked; window.__answers: what each Save stored; window.__saves. -->
|
|
<html lang="en"><head><meta charset="utf-8"><title>App Store Connect</title><script src="/_common.js"></script></head>
|
|
<body>
|
|
<nav aria-label="Apps"><ul><li><a href="#">App Information</a></li><li><a href="#">App Privacy</a></li><li><a href="#">Pricing and Availability</a></li></ul></nav>
|
|
<main>
|
|
<div><div><h2>App Privacy</h2></div><div><button type="button" id="publish">Publish</button></div></div>
|
|
<hr>
|
|
<div>
|
|
<div><h3><span>Privacy Policy</span></h3><button role="button" type="button" id="pp-edit">Edit</button>
|
|
<div><button role="button" type="button" aria-expanded="false" aria-haspopup="menu">English (U.S.)</button><button type="button" role="button">?</button></div></div>
|
|
<div><div><label for="privacyPolicyUrl">Privacy Policy URL</label><button type="button" role="button" aria-label="More information">?</button></div><p>https://example.com/privacy</p></div>
|
|
</div>
|
|
<hr>
|
|
<div><div><h3><span>Data Types</span></h3><button role="button" type="button" id="dt-edit">Edit</button></div><ul><li id="summary"></li></ul></div>
|
|
<div id="grid"></div>
|
|
</main>
|
|
<div id="dialogs"></div>
|
|
<script>
|
|
// [category, type label in the row (null: the row has only "Set Up"), the type in "Set Up <type>"]
|
|
const ROWS = [
|
|
['Location', 'Coarse Location', 'Coarse Location'],
|
|
['User Content', 'Emails or Text Messages', 'Emails or Text Messages'],
|
|
['Identifiers', 'Device ID', 'Device ID'],
|
|
['Purchases', null, 'Purchase History'],
|
|
['Usage Data', 'Product Interaction', 'Product Interaction'],
|
|
['Diagnostics', 'Crash Data', 'Crash Data'],
|
|
];
|
|
const PURPOSES = ['Third-Party Advertising', "Developer's Advertising or Marketing", 'Analytics', 'Product Personalization', 'App Functionality', 'Other Purposes'];
|
|
const q = new URLSearchParams(location.search);
|
|
const unknown = q.get('unknown');
|
|
const preset = Object.fromEntries(q.getAll('preset').map(v => { const [t, ps] = v.split(':'); return [t, ps ? ps.split('|') : []]; }));
|
|
const $ = id => document.getElementById(id);
|
|
window.__setups = []; window.__answers = {}; window.__saves = 0;
|
|
$('summary').textContent = `${ROWS.length} data types collected from this app: ${ROWS.map(r => r[2]).join(', ')}`;
|
|
let cat = null;
|
|
for (const [c, label, type] of ROWS) {
|
|
if (c !== cat) { cat = c; const h = document.createElement('div'); h.innerHTML = `<hr><div><div></div><p>${c}</p></div>`; $('grid').append(h); }
|
|
const row = document.createElement('div');
|
|
row.dataset.type = type;
|
|
row.innerHTML = (label ? `<div><p>${label}</p><div><button tabindex="0" role="button" type="button" aria-expanded="false" aria-label="More information">?</button></div></div>` : '') + '<div class="action"></div>';
|
|
$('grid').append(row);
|
|
if (preset[type]) setUpDone(type, preset[type]); else setUpOpen(type);
|
|
}
|
|
function setUpOpen(type) {
|
|
const a = document.querySelector(`[data-type="${type}"] .action`);
|
|
a.innerHTML = `<p>Set Up ${type}</p>`;
|
|
a.onclick = () => { window.__setups.push(a.textContent.trim()); openType(type); };
|
|
}
|
|
function setUpDone(type, purposes) {
|
|
const a = document.querySelector(`[data-type="${type}"] .action`);
|
|
a.onclick = null;
|
|
a.innerHTML = `<p>${purposes.join(', ')}</p><button type="button" role="button">Edit</button>`;
|
|
a.querySelector('button').addEventListener('click', () => openType(type));
|
|
}
|
|
// the per-type dialog: purposes > Next > linked > Next > tracking > Save
|
|
function openType(type) {
|
|
const low = type.toLowerCase();
|
|
const d = document.createElement('dialog');
|
|
d.setAttribute('role', 'dialog'); d.setAttribute('aria-modal', 'true'); d.setAttribute('aria-labelledby', 'modal-header');
|
|
const radio = (name, v, html) => `<div><input type="radio" name="${name}" id="${name}_${v}" value="${v}"><div><label for="${name}_${v}"><span>${html}</span></label></div></div>`;
|
|
const step2 = unknown === type
|
|
? `<p>Which kinds of identity does ${low} carry?</p><span role="switch" aria-checked="false" tabindex="0">Account identity</span><span role="switch" aria-checked="false" tabindex="0">Device identity</span>`
|
|
: radio('linked', 'yes', `<strong>Yes</strong>, ${low} collected from this app is linked to the user’s identity`) + radio('linked', 'no', `<strong>No</strong>, ${low} collected from this app is not linked to the user’s identity`);
|
|
d.innerHTML = `<div role="document"><h2 id="modal-header"><span>${type}</span></h2>`
|
|
+ `<div data-p="1"><p>Indicate how ${low} collected from this app is being used by you or your third-party partners.</p>`
|
|
+ PURPOSES.map((p, i) => `<div><input type="checkbox" id="purpose_${i}"><div><label for="purpose_${i}"><p>${p}</p><p>Description of ${p}</p></label></div></div>`).join('')
|
|
+ `<div><button type="button">Cancel</button><button type="button" data-go="2">Next</button></div></div>`
|
|
+ `<div data-p="2" hidden>${step2}<div><button type="button" data-go="1">Back</button><button type="button">Cancel</button><button type="button" data-go="3">Next</button></div></div>`
|
|
+ `<div data-p="3" hidden>`
|
|
+ radio('tracking', 'yes', `<strong>Yes</strong>, we use ${low} for tracking purposes`) + radio('tracking', 'no', `<strong>No</strong>, we do not use ${low} for tracking purposes`)
|
|
+ `<div><button type="button" data-go="2">Back</button><button type="button">Cancel</button><button type="button" data-save>Save</button></div></div></div>`;
|
|
d.setAttribute('open', '');
|
|
d.addEventListener('click', e => {
|
|
const go = e.target.closest('[data-go]')?.getAttribute('data-go');
|
|
if (go) d.querySelectorAll('[data-p]').forEach(p => { p.hidden = p.getAttribute('data-p') !== go; });
|
|
if (e.target.closest('button')?.textContent === 'Cancel') d.remove();
|
|
if (e.target.closest('[data-save]')) {
|
|
const purposes = PURPOSES.filter((p, i) => d.querySelector(`#purpose_${i}`).checked);
|
|
const val = n => d.querySelector(`input[name="${n}"]:checked`)?.value ?? null;
|
|
window.__answers[type] = { purposes, linked: val('linked'), tracking: val('tracking') };
|
|
window.__saves++;
|
|
d.remove();
|
|
setUpDone(type, purposes);
|
|
}
|
|
});
|
|
$('dialogs').append(d);
|
|
}
|
|
</script>
|
|
</body></html>
|