From 4ebb172cf5aadcfd994c37fcc555a5892847aa29 Mon Sep 17 00:00:00 2001 From: Natalie Bunduwongse Date: Fri, 25 Sep 2026 11:38:39 +1200 Subject: [PATCH] feat(audience): add mobile attribution checkboxes to build settings asset Replaces the manual "type AUDIENCE_MOBILE_ATTRIBUTION into Player Settings" step with two checkboxes (iOS, Android) on the existing AudienceMobileBuildSettings asset. The checkboxes read and write the real scripting define directly, nothing is cached or serialized, so a define set manually before this change still shows correctly and nothing resets on upgrade. Also removes three copies of the same define-check method, one per iOS post-processor, in favour of a single shared helper. Co-Authored-By: Claude Sonnet 5 --- .../AudienceMobileBuildSettingsEditor.cs | 36 +++++++++ .../AudienceMobileBuildSettingsEditor.cs.meta | 11 +++ .../Editor/MobileAttributionDefine.cs | 52 ++++++++++++ .../Editor/MobileAttributionDefine.cs.meta | 11 +++ .../Editor/iOSFrameworkPostProcessor.cs | 15 +--- .../Editor/iOSInfoPlistPostProcessor.cs | 23 +----- .../Editor/iOSPrivacyManifestPostProcessor.cs | 12 +-- .../Editor/MobileAttributionDefineTests.cs | 79 +++++++++++++++++++ .../MobileAttributionDefineTests.cs.meta | 11 +++ 9 files changed, 206 insertions(+), 44 deletions(-) create mode 100644 src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs create mode 100644 src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta create mode 100644 src/Packages/Audience/Editor/MobileAttributionDefine.cs create mode 100644 src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta create mode 100644 src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs create mode 100644 src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta diff --git a/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs new file mode 100644 index 00000000..c17aa1df --- /dev/null +++ b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs @@ -0,0 +1,36 @@ +#nullable enable + +using UnityEditor; + +namespace Immutable.Audience.Editor +{ + /// + /// Adds the mobile attribution toggles to . + /// + /// + /// The toggles read and write directly. + /// They are not fields on the asset, so an existing define set manually + /// (the old way) shows correctly the first time this asset is opened, + /// and nothing gets reset by updating the package. + /// + [CustomEditor(typeof(AudienceMobileBuildSettings))] + internal sealed class AudienceMobileBuildSettingsEditor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + EditorGUILayout.LabelField("Mobile Attribution", EditorStyles.boldLabel); + DrawToggle(BuildTargetGroup.iOS, "Enable for iOS"); + DrawToggle(BuildTargetGroup.Android, "Enable for Android"); + EditorGUILayout.Space(); + + DrawDefaultInspector(); + } + + private static void DrawToggle(BuildTargetGroup group, string label) + { + var enabled = MobileAttributionDefine.IsEnabled(group); + var toggled = EditorGUILayout.Toggle(label, enabled); + if (toggled != enabled) MobileAttributionDefine.SetEnabled(group, toggled); + } + } +} diff --git a/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta new file mode 100644 index 00000000..4fbdee1c --- /dev/null +++ b/src/Packages/Audience/Editor/AudienceMobileBuildSettingsEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fdd843fb8feb4b1e9cc8c97d88748c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Audience/Editor/MobileAttributionDefine.cs b/src/Packages/Audience/Editor/MobileAttributionDefine.cs new file mode 100644 index 00000000..8843abbc --- /dev/null +++ b/src/Packages/Audience/Editor/MobileAttributionDefine.cs @@ -0,0 +1,52 @@ +#nullable enable + +using System.Collections.Generic; +using UnityEditor; + +namespace Immutable.Audience.Editor +{ + /// + /// Reads and writes the AUDIENCE_MOBILE_ATTRIBUTION scripting + /// define on Player Settings. + /// + /// + /// No state is cached or serialized here. Player Settings is the only + /// source of truth, so this always reflects whatever a studio has + /// already set, including manually, and nothing here can go stale. + /// + internal static class MobileAttributionDefine + { + internal const string Symbol = "AUDIENCE_MOBILE_ATTRIBUTION"; + + internal static bool IsEnabled(BuildTargetGroup group) => + Contains(PlayerSettings.GetScriptingDefineSymbolsForGroup(group)); + + internal static void SetEnabled(BuildTargetGroup group, bool enabled) + { + var current = PlayerSettings.GetScriptingDefineSymbolsForGroup(group); + PlayerSettings.SetScriptingDefineSymbolsForGroup(group, WithSymbol(current, enabled)); + } + + internal static bool Contains(string? defines) + { + foreach (var define in (defines ?? string.Empty).Split(';')) + { + if (define.Trim() == Symbol) return true; + } + return false; + } + + internal static string WithSymbol(string? defines, bool enabled) + { + var result = new List(); + foreach (var define in (defines ?? string.Empty).Split(';', System.StringSplitOptions.RemoveEmptyEntries)) + { + var trimmed = define.Trim(); + if (trimmed.Length == 0 || trimmed == Symbol) continue; + result.Add(trimmed); + } + if (enabled) result.Add(Symbol); + return string.Join(";", result); + } + } +} diff --git a/src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta b/src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta new file mode 100644 index 00000000..bb0f58b6 --- /dev/null +++ b/src/Packages/Audience/Editor/MobileAttributionDefine.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 225fcb5f29be241f4a71fb94096a09a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs b/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs index c4c1f4b7..3fdb1808 100644 --- a/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs +++ b/src/Packages/Audience/Editor/iOSFrameworkPostProcessor.cs @@ -50,7 +50,7 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr if (target != BuildTarget.iOS) return; #if UNITY_IOS - if (!AttributionDefineEnabled()) return; + if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) return; var pbxPath = PBXProject.GetPBXProjectPath(pathToBuiltProject); if (!File.Exists(pbxPath)) @@ -75,18 +75,5 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr pbx.WriteToFile(pbxPath); #endif } - - // Reads the iOS-target define list specifically. The post-processor - // mutates iOS build output regardless of which target the editor is - // currently focused on. - 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; - } } } diff --git a/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs b/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs index 4061373a..92718fb7 100644 --- a/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs +++ b/src/Packages/Audience/Editor/iOSInfoPlistPostProcessor.cs @@ -34,7 +34,6 @@ namespace Immutable.Audience.Editor internal static class iOSInfoPlistPostProcessor { internal const int CallbackOrder = 9050; - internal const string AttributionDefine = "AUDIENCE_MOBILE_ATTRIBUTION"; [PostProcessBuild(CallbackOrder)] internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject) @@ -42,7 +41,7 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr if (target != BuildTarget.iOS) return; #if UNITY_IOS - if (!AttributionDefineEnabled()) return; + if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) return; var plistPath = Path.Combine(pathToBuiltProject, "Info.plist"); if (!File.Exists(plistPath)) @@ -68,12 +67,11 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr [MenuItem("Tools/Immutable/Audience/Validate iOS Build Settings")] private static void ValidateBuildSettings() { - if (!AttributionDefineEnabled()) + if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) { Debug.LogWarning( - "[ImmutableAudience] AUDIENCE_MOBILE_ATTRIBUTION scripting define is not set " + - "for the iOS player target. The post-processor will not modify Info.plist. " + - "Add the define under Player Settings → Other Settings → Scripting Define Symbols."); + "[ImmutableAudience] Mobile attribution is not enabled for iOS. The post-processor " + + "will not modify Info.plist. Enable it on the AudienceMobileBuildSettings asset."); return; } @@ -92,19 +90,6 @@ private static void ValidateBuildSettings() : string.Concat(System.Array.ConvertAll(ids, id => $" - {id}\n")))); } - // Reads the iOS-target define list specifically. The post-processor - // mutates iOS build output regardless of which target the editor is - // currently focused on. - private static bool AttributionDefineEnabled() - { - var defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS) ?? string.Empty; - foreach (var define in defines.Split(';')) - { - if (define.Trim() == AttributionDefine) return true; - } - return false; - } - #if UNITY_IOS internal static void ApplyTrackingUsageDescription( PlistElementDict root, diff --git a/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs b/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs index 16542cf8..96b89ec9 100644 --- a/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs +++ b/src/Packages/Audience/Editor/iOSPrivacyManifestPostProcessor.cs @@ -29,7 +29,7 @@ internal static void OnPostProcessBuild(BuildTarget target, string pathToBuiltPr if (target != BuildTarget.iOS) return; #if UNITY_IOS - if (!AttributionDefineEnabled()) return; + if (!MobileAttributionDefine.IsEnabled(BuildTargetGroup.iOS)) return; var builtManifestPath = FindBuiltManifest(pathToBuiltProject); if (builtManifestPath == null) @@ -119,15 +119,5 @@ v is PlistElementString s && 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; - } } } diff --git a/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs new file mode 100644 index 00000000..ce737056 --- /dev/null +++ b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs @@ -0,0 +1,79 @@ +#nullable enable + +using NUnit.Framework; +using Immutable.Audience.Editor; + +namespace Immutable.Audience.Editor.Tests +{ + [TestFixture] + internal class MobileAttributionDefineTests + { + [Test] + public void Contains_NullOrEmpty_ReturnsFalse() + { + Assert.IsFalse(MobileAttributionDefine.Contains(null)); + Assert.IsFalse(MobileAttributionDefine.Contains("")); + } + + [Test] + public void Contains_SymbolAmongOthers_ReturnsTrue() + { + Assert.IsTrue(MobileAttributionDefine.Contains("FOO;AUDIENCE_MOBILE_ATTRIBUTION;BAR")); + } + + [Test] + public void Contains_SymbolWithStrayWhitespace_ReturnsTrue() + { + Assert.IsTrue(MobileAttributionDefine.Contains("FOO; AUDIENCE_MOBILE_ATTRIBUTION ;BAR")); + } + + [Test] + public void Contains_OtherDefinesOnly_ReturnsFalse() + { + Assert.IsFalse(MobileAttributionDefine.Contains("FOO;BAR")); + } + + [Test] + public void WithSymbol_EnableOnEmpty_AddsOnlySymbol() + { + Assert.AreEqual("AUDIENCE_MOBILE_ATTRIBUTION", MobileAttributionDefine.WithSymbol(null, enabled: true)); + } + + [Test] + public void WithSymbol_EnableWithOthers_KeepsOthersAndAddsSymbol() + { + var result = MobileAttributionDefine.WithSymbol("FOO;BAR", enabled: true); + + Assert.IsTrue(MobileAttributionDefine.Contains(result)); + Assert.IsTrue(result.Contains("FOO")); + Assert.IsTrue(result.Contains("BAR")); + } + + [Test] + public void WithSymbol_EnableWhenAlreadyPresent_DoesNotDuplicate() + { + var result = MobileAttributionDefine.WithSymbol("FOO;AUDIENCE_MOBILE_ATTRIBUTION;BAR", enabled: true); + + var symbolCount = System.Array.FindAll(result.Split(';'), d => d == "AUDIENCE_MOBILE_ATTRIBUTION").Length; + Assert.AreEqual(1, symbolCount); + } + + [Test] + public void WithSymbol_DisableRemovesSymbolOnly() + { + var result = MobileAttributionDefine.WithSymbol("FOO;AUDIENCE_MOBILE_ATTRIBUTION;BAR", enabled: false); + + Assert.IsFalse(MobileAttributionDefine.Contains(result)); + Assert.IsTrue(result.Contains("FOO")); + Assert.IsTrue(result.Contains("BAR")); + } + + [Test] + public void WithSymbol_DisableWhenAbsent_LeavesOthersUnchanged() + { + var result = MobileAttributionDefine.WithSymbol("FOO;BAR", enabled: false); + + Assert.AreEqual("FOO;BAR", result); + } + } +} diff --git a/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta new file mode 100644 index 00000000..7b3edd11 --- /dev/null +++ b/src/Packages/Audience/Tests/Editor/MobileAttributionDefineTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b4d32623b1c9476a9a1775f96542ab2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: