Skip to content

Commit 10d8092

Browse files
authored
Merge pull request #773 from immutable/feat/sdk-315-ios-privacy-manifest
feat(audience-sdk): ship iOS PrivacyInfo.xcprivacy privacy manifest (SDK-315)
2 parents 773eb68 + d26fa41 commit 10d8092

6 files changed

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

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)