Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#nullable enable

using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEngine;
#if UNITY_IOS
using UnityEditor.iOS.Xcode;
#endif

namespace Immutable.Audience.Editor
{
/// <summary>
/// Patches <c>UnityFramework/PrivacyInfo.xcprivacy</c> to add IDFA tracking
/// declarations when <c>AUDIENCE_MOBILE_ATTRIBUTION</c> is enabled.
/// Unity auto-merges the default manifest; this post-processor adds
/// <c>NSPrivacyTracking=true</c> and <c>NSPrivacyCollectedDataTypeAdvertisingData</c>
/// in-place so Unity's own Required Reason API entries are preserved.
/// Runs at callbackOrder 9052, after the Info.plist (9050) and framework (9051) post-processors.
/// </summary>
internal static class iOSPrivacyManifestPostProcessor
{
internal const int CallbackOrder = 9052;
private const string BuiltManifestName = "PrivacyInfo.xcprivacy";

[PostProcessBuild(CallbackOrder)]
internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject)
{
if (target != BuildTarget.iOS) return;

#if UNITY_IOS
if (!AttributionDefineEnabled()) return;

var builtManifestPath = FindBuiltManifest(pathToBuiltProject);
if (builtManifestPath == null)
{
Debug.LogWarning(
$"[ImmutableAudience] iOS privacy manifest post-processor: {BuiltManifestName} not found " +
$"under {pathToBuiltProject}. Skipping attribution manifest update.");
return;
}

var plist = new PlistDocument();
plist.ReadFromFile(builtManifestPath);
ApplyAttributionPrivacyEntries(plist.root);
plist.WriteToFile(builtManifestPath);
#endif
}

private static string? FindBuiltManifest(string pathToBuiltProject)
{
// Unity 2019.3+ places the merged manifest here.
var candidate = Path.Combine(pathToBuiltProject, "UnityFramework", BuiltManifestName);
if (File.Exists(candidate)) return candidate;

// Fall back to a recursive search for older Unity layout variants.
var found = Directory.GetFiles(pathToBuiltProject, BuiltManifestName, SearchOption.AllDirectories);
return found.Length > 0 ? found[0] : null;
}

#if UNITY_IOS
/// <summary>
/// Adds the attribution-specific privacy declarations to an existing
/// (already Unity-merged) <c>PrivacyInfo.xcprivacy</c> plist root.
/// Idempotent — safe to call on a manifest that already has these entries.
/// </summary>
internal static void ApplyAttributionPrivacyEntries(PlistElementDict root)
{
// IDFA collection constitutes tracking under Apple's definition.
root.SetBoolean("NSPrivacyTracking", true);

PlistElementArray dataTypes;
if (root.values.TryGetValue("NSPrivacyCollectedDataTypes", out var existing) &&
existing is PlistElementArray existingArray)
{
dataTypes = existingArray;
}
else
{
dataTypes = root.CreateArray("NSPrivacyCollectedDataTypes");
}

// Avoid duplicate entries if the post-processor is re-run.
const string advertisingType = "NSPrivacyCollectedDataTypeAdvertisingData";
foreach (var item in dataTypes.values)
{
if (item is PlistElementDict d &&
d.values.TryGetValue("NSPrivacyCollectedDataType", out var v) &&
v is PlistElementString s &&
s.value == advertisingType)
return;
}

var entry = dataTypes.AddDict();
entry.SetString("NSPrivacyCollectedDataType", advertisingType);
entry.SetBoolean("NSPrivacyCollectedDataTypeLinked", true);
entry.SetBoolean("NSPrivacyCollectedDataTypeTracking", true);
var purposes = entry.CreateArray("NSPrivacyCollectedDataTypePurposes");
purposes.AddString("NSPrivacyCollectedDataTypePurposeAnalytics");
}
#endif

private static bool AttributionDefineEnabled()
{
var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS) ?? string.Empty;
foreach (var define in defines.Split(';'))
{
if (define.Trim() == iOSInfoPlistPostProcessor.AttributionDefine) return true;
}
return false;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<!-- IDFV: vendor-scoped device identifier, linked to user, analytics only. -->
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeDeviceID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
<dict>
<!-- IDFA: cross-app advertising identifier, linked to user, used for tracking. -->
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeAdvertisingData</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<true/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<!-- No Required Reason APIs used by this SDK. -->
<key>NSPrivacyAccessedAPITypes</key>
<array/>
</dict>
</plist>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions src/Packages/Audience/Runtime/Plugins/iOS/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<!-- IDFV: vendor-scoped device identifier, linked to user, analytics only. -->
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeDeviceID</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<!-- No Required Reason APIs used by this SDK. -->
<key>NSPrivacyAccessedAPITypes</key>
<array/>
</dict>
</plist>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading