ITMS-91064: NSPrivacyTracking and NSPrivacyTrackingDomains must agree in every privacy manifest (true needs domains, domains need true); check, scan and privacy-manifest fail on either mismatch with the exact ITMS text; App Privacy tracking no longer demands the app manifest's true: SDK manifests that declare tracking (Pods, frameworks, SwiftPM, Carthage, plugins, the built IPA) are info, known tracking SDKs info, else a warning; privacy-manifest --propose writes true only with appPrivacy.trackingDomains; apple upload checks every manifest in the IPA (XML or binary) before any network call, apple build after the export. Pitfall 29
This commit is contained in:
parent
b7bfe89151
commit
c8213be2b6
19 changed files with 631 additions and 44 deletions
|
|
@ -109,21 +109,43 @@ export function validateAppPrivacyTypes(types: unknown): string[] {
|
|||
return e;
|
||||
}
|
||||
|
||||
export interface PrivacyMismatch { level: 'error' | 'warn'; msg: string }
|
||||
export interface PrivacyMismatch { level: 'error' | 'warn' | 'info'; msg: string }
|
||||
|
||||
/** Where tracking is declared besides the app's own manifest. */
|
||||
export interface TrackingSources {
|
||||
/** SDKs whose own PrivacyInfo.xcprivacy declares NSPrivacyTracking true (Pods, frameworks, plugins, built app). */
|
||||
sdkManifests?: string[];
|
||||
/** Tracking-capable SDKs found in the project's dependencies (ads, mediation, attribution) whose manifest was not
|
||||
* found locally (Pods not installed, binary only in the build). */
|
||||
trackerSdks?: string[];
|
||||
}
|
||||
|
||||
/** App Privacy "used for tracking" against the privacy manifests. The app's own manifest does NOT have to say
|
||||
* NSPrivacyTracking true when the tracking comes from an SDK: the SDK's manifest declares it (and true with an empty
|
||||
* NSPrivacyTrackingDomains is ITMS-91064). `what`: the App Privacy answer, e.g. "appPrivacy.tracking is true". */
|
||||
export function trackingDeclaredIssues(what: string, appManifest: { path: string; tracking: boolean | null } | null, src: TrackingSources): PrivacyMismatch[] {
|
||||
if (appManifest?.tracking === true) return [];
|
||||
const own = appManifest ? `${appManifest.path} says NSPrivacyTracking ${appManifest.tracking === false ? 'false' : 'missing'}` : 'no app manifest';
|
||||
const by = [...new Set(src.sdkManifests ?? [])];
|
||||
if (by.length) return [{ level: 'info', msg: `${what}: tracking declared by the ${by.join(', ')} manifest${by.length > 1 ? 's' : ''} (${own}, right when only the SDK tracks)` }];
|
||||
const known = [...new Set(src.trackerSdks ?? [])];
|
||||
if (known.length) return [{ level: 'info', msg: `${what}: tracking comes from ${known.join(', ')}; its own privacy manifest declares it (not found in this folder: install Pods / resolve packages, or \`storebound apple upload --dry\` checks the built app). ${own}, right when only the SDK tracks` }];
|
||||
return [{ level: 'warn', msg: `${what}, but no privacy manifest declares NSPrivacyTracking true (${own}, and no SDK manifest that declares tracking was found): check that the tracking SDK ships a manifest that declares it. Do not set the app's manifest to true without NSPrivacyTrackingDomains (ITMS-91064)` }];
|
||||
}
|
||||
|
||||
/**
|
||||
* `appPrivacy.types` against the iOS project (both ways): any type used for tracking <-> Info.plist
|
||||
* NSUserTrackingUsageDescription and the app manifest's NSPrivacyTracking true; the app manifest's
|
||||
* NSPrivacyCollectedDataTypes <-> the types marked collected. `manifest` null: no PrivacyInfo.xcprivacy found.
|
||||
* NSUserTrackingUsageDescription and a manifest that declares tracking (the app's, or an SDK's: `tracking`); the app
|
||||
* manifest's NSPrivacyCollectedDataTypes <-> the types marked collected. `manifest` null: no PrivacyInfo.xcprivacy found.
|
||||
*/
|
||||
export function appPrivacyMismatches(types: Partial<Record<AppleDataTypeId, AppleTypeAnswer>>, ios: { attString: boolean; manifest: { path: string; tracking: boolean | null; collectedTypes: string[] } | null }): PrivacyMismatch[] {
|
||||
export function appPrivacyMismatches(types: Partial<Record<AppleDataTypeId, AppleTypeAnswer>>, ios: { attString: boolean; manifest: { path: string; tracking: boolean | null; collectedTypes: string[] } | null; tracking?: TrackingSources }): PrivacyMismatch[] {
|
||||
const out: PrivacyMismatch[] = [];
|
||||
const entries = Object.entries(types) as [AppleDataTypeId, AppleTypeAnswer][];
|
||||
const tracked = entries.filter(([, a]) => a.tracking === true).map(([id]) => id);
|
||||
const m = ios.manifest;
|
||||
if (tracked.length) {
|
||||
if (!ios.attString) out.push({ level: 'error', msg: `appPrivacy.types says ${tracked.join(', ')} ${tracked.length === 1 ? 'is' : 'are'} used for tracking, but Info.plist has no NSUserTrackingUsageDescription (App Tracking Transparency is required before tracking)` });
|
||||
if (m && m.tracking !== true) out.push({ level: 'error', msg: `appPrivacy.types says ${tracked.join(', ')} ${tracked.length === 1 ? 'is' : 'are'} used for tracking, but ${m.path} has NSPrivacyTracking ${m.tracking === false ? 'false' : 'missing'}` });
|
||||
if (m) out.push(...trackingDeclaredIssues(`appPrivacy.types says ${tracked.join(', ')} ${tracked.length === 1 ? 'is' : 'are'} used for tracking`, m, ios.tracking ?? {}));
|
||||
} else if (entries.some(([, a]) => a.tracking === false) && entries.every(([, a]) => a.collected === false || a.tracking === false)) {
|
||||
if (ios.attString) out.push({ level: 'warn', msg: 'Info.plist has NSUserTrackingUsageDescription, but appPrivacy.types marks no collected type as used for tracking' });
|
||||
if (m?.tracking === true) out.push({ level: 'warn', msg: `${m.path} has NSPrivacyTracking true, but appPrivacy.types marks no collected type as used for tracking` });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue