Skip to content

Commit 72753c3

Browse files
nattb8claude
andcommitted
feat(audience-sdk): ship iOS PrivacyInfo.xcprivacy privacy manifest (SDK-315)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ca4e26b commit 72753c3

6 files changed

Lines changed: 268 additions & 0 deletions
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#nullable enable
2+
3+
using System.IO;
4+
using UnityEditor;
5+
using UnityEditor.Callbacks;
6+
using UnityEngine;
7+
#if UNITY_IOS
8+
using UnityEditor.iOS.Xcode;
9+
#endif
10+
11+
namespace Immutable.Audience.Editor
12+
{
13+
/// <summary>
14+
/// Patches the <c>PrivacyInfo.xcprivacy</c> that Unity merges into the
15+
/// generated iOS Xcode project when <c>AUDIENCE_MOBILE_ATTRIBUTION</c>
16+
/// is enabled.
17+
/// </summary>
18+
/// <remarks>
19+
/// Unity 6 auto-merges plugin <c>PrivacyInfo.xcprivacy</c> files (from
20+
/// <c>Runtime/Plugins/iOS/</c>) into <c>UnityFramework/PrivacyInfo.xcprivacy</c>
21+
/// alongside Unity's own Required Reason API entries. When attribution is on,
22+
/// the SDK additionally collects IDFA, so two extra declarations are needed:
23+
/// <c>NSPrivacyTracking = true</c> and <c>NSPrivacyCollectedDataTypeAdvertisingData</c>.
24+
/// This post-processor adds those fields in-place so Unity's own entries are
25+
/// preserved.
26+
///
27+
/// <c>callbackOrder = 9052</c> runs just after the framework post-processor (9051)
28+
/// and the Info.plist post-processor (9050).
29+
/// </remarks>
30+
internal static class iOSPrivacyManifestPostProcessor
31+
{
32+
internal const int CallbackOrder = 9052;
33+
private const string BuiltManifestName = "PrivacyInfo.xcprivacy";
34+
35+
[PostProcessBuild(CallbackOrder)]
36+
internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject)
37+
{
38+
if (target != BuildTarget.iOS) return;
39+
40+
#if UNITY_IOS
41+
if (!AttributionDefineEnabled()) return;
42+
43+
var builtManifestPath = FindBuiltManifest(pathToBuiltProject);
44+
if (builtManifestPath == null)
45+
{
46+
Debug.LogWarning(
47+
$"[ImmutableAudience] iOS privacy manifest post-processor: {BuiltManifestName} not found " +
48+
$"under {pathToBuiltProject}. Skipping attribution manifest update.");
49+
return;
50+
}
51+
52+
var plist = new PlistDocument();
53+
plist.ReadFromFile(builtManifestPath);
54+
ApplyAttributionPrivacyEntries(plist.root);
55+
plist.WriteToFile(builtManifestPath);
56+
#endif
57+
}
58+
59+
private static string? FindBuiltManifest(string pathToBuiltProject)
60+
{
61+
// Unity 2019.3+ places the merged manifest here.
62+
var candidate = Path.Combine(pathToBuiltProject, "UnityFramework", BuiltManifestName);
63+
if (File.Exists(candidate)) return candidate;
64+
65+
// Fall back to a recursive search for older Unity layout variants.
66+
var found = Directory.GetFiles(pathToBuiltProject, BuiltManifestName, SearchOption.AllDirectories);
67+
return found.Length > 0 ? found[0] : null;
68+
}
69+
70+
#if UNITY_IOS
71+
/// <summary>
72+
/// Adds the attribution-specific privacy declarations to an existing
73+
/// (already Unity-merged) <c>PrivacyInfo.xcprivacy</c> plist root.
74+
/// Idempotent — safe to call on a manifest that already has these entries.
75+
/// </summary>
76+
internal static void ApplyAttributionPrivacyEntries(PlistElementDict root)
77+
{
78+
// IDFA collection constitutes tracking under Apple's definition.
79+
root.SetBoolean("NSPrivacyTracking", true);
80+
81+
PlistElementArray dataTypes;
82+
if (root.values.TryGetValue("NSPrivacyCollectedDataTypes", out var existing) &&
83+
existing is PlistElementArray existingArray)
84+
{
85+
dataTypes = existingArray;
86+
}
87+
else
88+
{
89+
dataTypes = root.CreateArray("NSPrivacyCollectedDataTypes");
90+
}
91+
92+
// Avoid duplicate entries if the post-processor is re-run.
93+
const string advertisingType = "NSPrivacyCollectedDataTypeAdvertisingData";
94+
foreach (var item in dataTypes.values)
95+
{
96+
if (item is PlistElementDict d &&
97+
d.values.TryGetValue("NSPrivacyCollectedDataType", out var v) &&
98+
v is PlistElementString s &&
99+
s.value == advertisingType)
100+
return;
101+
}
102+
103+
var entry = dataTypes.AddDict();
104+
entry.SetString("NSPrivacyCollectedDataType", advertisingType);
105+
entry.SetBoolean("NSPrivacyCollectedDataTypeLinked", true);
106+
entry.SetBoolean("NSPrivacyCollectedDataTypeTracking", true);
107+
var purposes = entry.CreateArray("NSPrivacyCollectedDataTypePurposes");
108+
purposes.AddString("NSPrivacyCollectedDataTypePurposeAnalytics");
109+
}
110+
#endif
111+
112+
private static bool AttributionDefineEnabled()
113+
{
114+
var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS) ?? string.Empty;
115+
foreach (var define in defines.Split(';'))
116+
{
117+
if (define.Trim() == iOSInfoPlistPostProcessor.AttributionDefine) return true;
118+
}
119+
return false;
120+
}
121+
}
122+
}

src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSPrivacyTracking</key>
6+
<true/>
7+
<key>NSPrivacyTrackingDomains</key>
8+
<array/>
9+
<key>NSPrivacyCollectedDataTypes</key>
10+
<array>
11+
<dict>
12+
<!-- IDFV: vendor-scoped device identifier, linked to user, analytics only. -->
13+
<key>NSPrivacyCollectedDataType</key>
14+
<string>NSPrivacyCollectedDataTypeDeviceID</string>
15+
<key>NSPrivacyCollectedDataTypeLinked</key>
16+
<true/>
17+
<key>NSPrivacyCollectedDataTypeTracking</key>
18+
<false/>
19+
<key>NSPrivacyCollectedDataTypePurposes</key>
20+
<array>
21+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
22+
</array>
23+
</dict>
24+
<dict>
25+
<!-- IDFA: cross-app advertising identifier, linked to user, used for tracking. -->
26+
<key>NSPrivacyCollectedDataType</key>
27+
<string>NSPrivacyCollectedDataTypeAdvertisingData</string>
28+
<key>NSPrivacyCollectedDataTypeLinked</key>
29+
<true/>
30+
<key>NSPrivacyCollectedDataTypeTracking</key>
31+
<true/>
32+
<key>NSPrivacyCollectedDataTypePurposes</key>
33+
<array>
34+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
35+
</array>
36+
</dict>
37+
</array>
38+
<!-- No Required Reason APIs used by this SDK. -->
39+
<key>NSPrivacyAccessedAPITypes</key>
40+
<array/>
41+
</dict>
42+
</plist>

src/Packages/Audience/Runtime/Plugins/iOS/PrivacyInfo.attribution.xcprivacy.meta

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSPrivacyTracking</key>
6+
<false/>
7+
<key>NSPrivacyTrackingDomains</key>
8+
<array/>
9+
<key>NSPrivacyCollectedDataTypes</key>
10+
<array>
11+
<dict>
12+
<!-- IDFV: vendor-scoped device identifier, linked to user, analytics only. -->
13+
<key>NSPrivacyCollectedDataType</key>
14+
<string>NSPrivacyCollectedDataTypeDeviceID</string>
15+
<key>NSPrivacyCollectedDataTypeLinked</key>
16+
<true/>
17+
<key>NSPrivacyCollectedDataTypeTracking</key>
18+
<false/>
19+
<key>NSPrivacyCollectedDataTypePurposes</key>
20+
<array>
21+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
22+
</array>
23+
</dict>
24+
</array>
25+
<!-- No Required Reason APIs used by this SDK. -->
26+
<key>NSPrivacyAccessedAPITypes</key>
27+
<array/>
28+
</dict>
29+
</plist>

src/Packages/Audience/Runtime/Plugins/iOS/PrivacyInfo.xcprivacy.meta

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)