From 4c2e6c58bd896f7a2804820ba2b16c55b0cd3836 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 00:55:49 -0700 Subject: [PATCH 1/9] feat(demo): add iOS notification action icons --- .../App/Editor/iOS/BuildPostProcessor.cs | 59 +++++++++++++++++- examples/demo/README.md | 11 ++++ .../template-bookmark-icon.png | Bin 0 -> 312 bytes .../NotificationIcons/template-share-icon.png | Bin 0 -> 309 bytes examples/demo/run-android.sh | 10 ++- examples/demo/run-ios.sh | 17 ++++- 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 examples/demo/iOS/NotificationIcons/template-bookmark-icon.png create mode 100644 examples/demo/iOS/NotificationIcons/template-share-icon.png diff --git a/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs b/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs index e1ecfeb4..ee121293 100644 --- a/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs +++ b/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs @@ -56,12 +56,23 @@ public class BuildPostProcessor : IPostprocessBuildWithReport private static readonly string SoundsSourceDir = Path.Combine("iOS", "Sounds"); private static readonly string[] CustomSoundFiles = new string[] { "vine_boom.wav" }; + private static readonly string NotificationIconsSourceDir = Path.Combine( + "iOS", + "NotificationIcons" + ); + private static readonly string[] NotificationIconFiles = new string[] + { + "template-bookmark-icon.png", + "template-share-icon.png", + }; + private const string NotificationServiceExtensionTargetName = + "OneSignalNotificationServiceExtension"; /// /// must be between 40 and 50 to ensure that it's not overriden by Podfile generation (40) and that it's /// added before "pod install" (50) /// - public int callbackOrder => 45; + public int callbackOrder => 46; public void OnPostprocessBuild(BuildReport report) { @@ -80,6 +91,7 @@ public void OnPostprocessBuild(BuildReport report) EnableAppForLiveActivities(report.summary.outputPath); CreateWidgetExtension(report.summary.outputPath); AddCustomSoundsToMainTarget(report.summary.outputPath); + AddNotificationIconsToTargets(report.summary.outputPath); Debug.Log("BuildPostProcessor.OnPostprocessBuild complete"); } @@ -143,6 +155,51 @@ static void AddCustomSoundsToMainTarget(string outputPath) project.WriteToFile(projectPath); } + static void AddNotificationIconsToTargets(string outputPath) + { + var project = new PBXProject(); + var projectPath = PBXProject.GetPBXProjectPath(outputPath); + project.ReadFromString(File.ReadAllText(projectPath)); + + var mainTargetGuid = project.GetUnityMainTargetGuid(); + var extensionTargetGuid = project.TargetGuidByName( + NotificationServiceExtensionTargetName + ); + + if (string.IsNullOrEmpty(extensionTargetGuid)) + { + Debug.LogWarning( + $"{NotificationServiceExtensionTargetName} target is missing; notification icons will only be bundled with the main app." + ); + } + + foreach (var fileName in NotificationIconFiles) + { + var sourcePath = Path.Combine(NotificationIconsSourceDir, fileName); + if (!File.Exists(sourcePath)) + { + Debug.LogWarning( + $"Notification action icon missing at {sourcePath}; skipping iOS bundling." + ); + continue; + } + + var destAbsolutePath = Path.Combine(outputPath, fileName); + File.Copy(sourcePath, destAbsolutePath, true); + + var fileGuid = project.FindFileGuidByProjectPath(fileName); + if (string.IsNullOrEmpty(fileGuid)) + fileGuid = project.AddFile(fileName, fileName); + + project.AddFileToBuild(mainTargetGuid, fileGuid); + + if (!string.IsNullOrEmpty(extensionTargetGuid)) + project.AddFileToBuild(extensionTargetGuid, fileGuid); + } + + project.WriteToFile(projectPath); + } + static void AddWidgetExtensionToProject(string outputPath) { var project = new PBXProject(); diff --git a/examples/demo/README.md b/examples/demo/README.md index a4e288c0..03475328 100644 --- a/examples/demo/README.md +++ b/examples/demo/README.md @@ -73,3 +73,14 @@ Then configure your play settings and set `Target SDK` to `Simulator SDK` Then click `Build and Run` and select a location for the Xcode project. Or click `Build` to use your own simulator. ![iOS Example 2](docs/ios-example-2.png) + +### iOS action button icons + +The generated Xcode project bundles shared action button icons in both the main app and `OneSignalNotificationServiceExtension` targets. + +Create an image notification in the OneSignal dashboard with these action buttons: + +- **Save Story** with icon `template-bookmark-icon` +- **Share** with icon `template-share-icon` + +Enter the icon names without the `.png` extension. On an iOS 15 or newer physical device, expand the notification and verify that both icons render. diff --git a/examples/demo/iOS/NotificationIcons/template-bookmark-icon.png b/examples/demo/iOS/NotificationIcons/template-bookmark-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..99281835ab36dd51c94f0a6681923ae2d81e85b9 GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjZ#`WcLn;{GOu5L}Y#`9K)uN#` zyr8x3ftu{Xy9?$nxVvDrg_HS-W+%C~42-|$tFV473hdIKG-3bVn18$Z`A#a{tavN3 zVC%YD+hnhGWbMe@tZZ~lH;P@ z|GIXqtKM;(|D*Pen^GU)>fdX5vD*0dY8XPCcxc=#c& z;EGRs7nS%6F{U)U+g8|HamQl*0+H92YAy2e51MDqn&n`A6{16>#Z9${A^?lbS_C`Nx)9~j$m5%3^`}Zp{J-@>x`EuoK1x9_r z8qYG5r~A7V7@6h>?DlkNV*4S`>EV#!wKDqjGKuA`Ovz`gd?#Oxy3ff{#__PpxT`Qq z#o?jNdH)wNYqqYx^ZZTMf$I9+>iXx0M4wx32#kIf#!~pjedd+d+q1Npn7V(5s)%Tt z&UK!i{$w-DlntTsERWc}Z&kV@)^Ve3+J^Os}GRD2J$sCgTv!?!^P)zopr E0G6P9v;Y7A literal 0 HcmV?d00001 diff --git a/examples/demo/run-android.sh b/examples/demo/run-android.sh index 670a4442..f23d1e28 100755 --- a/examples/demo/run-android.sh +++ b/examples/demo/run-android.sh @@ -41,6 +41,8 @@ ADB="$(find_adb)" OUTPUT="$SCRIPT_DIR/Build/Android/onesignal-demo.apk" LOG="$SCRIPT_DIR/Build/build-android.log" +APP_BUNDLE_ID="com.onesignal.example" +APP_ACTIVITY="com.unity3d.player.UnityPlayerActivity" INSTALL=true SKIP_BUILD=false @@ -100,5 +102,11 @@ if [ "$INSTALL" = true ] && [ -n "$EMULATOR" ]; then "$ADB" -s "$EMULATOR" wait-for-device echo "Installing on $EMULATOR..." "$ADB" -s "$EMULATOR" install -r "$OUTPUT" - "$ADB" -s "$EMULATOR" shell am start -n com.onesignal.example/com.unity3d.player.UnityPlayerActivity + "$ADB" -s "$EMULATOR" shell am start -W -n "$APP_BUNDLE_ID/$APP_ACTIVITY" + + APP_PID=$("$ADB" -s "$EMULATOR" shell pidof -s "$APP_BUNDLE_ID" | tr -d '\r') + [ -z "$APP_PID" ] && echo "App launched, but its process could not be found." && exit 1 + + echo "Console attached (press Ctrl-C to detach)..." + "$ADB" -s "$EMULATOR" logcat --pid="$APP_PID" fi diff --git a/examples/demo/run-ios.sh b/examples/demo/run-ios.sh index 835e6c3f..228e9b51 100755 --- a/examples/demo/run-ios.sh +++ b/examples/demo/run-ios.sh @@ -151,9 +151,19 @@ else echo "" START=$(date +%s) - "$UNITY" -batchmode -nographics -quit -buildTarget iOS \ + if ! "$UNITY" -batchmode -nographics -quit -buildTarget iOS \ -projectPath "$SCRIPT_DIR" -executeMethod BuildScript.BuildiOSSimulator \ - -logFile "$LOG" + -logFile "$LOG"; then + ELAPSED=$(( $(date +%s) - START )) + if grep -q "No valid Unity Editor license found" "$LOG"; then + echo "Unity build failed: no valid Unity Editor license is active." + echo "Open Unity Hub, sign in and activate a license, then re-run this script." + else + echo "Unity build failed after $((ELAPSED/60))m $((ELAPSED%60))s." + echo "Check the log for details: $LOG" + fi + exit 1 + fi ELAPSED=$(( $(date +%s) - START )) [ ! -d "$XCODE_DIR/Unity-iPhone.xcodeproj" ] && echo "Build failed after $((ELAPSED/60))m $((ELAPSED%60))s. Check $LOG" && exit 1 @@ -193,5 +203,6 @@ if [ "$INSTALL" = true ] && [ -n "$SIM_UDID" ]; then echo "Installing on $SIM_NAME..." xcrun simctl install "$SIM_UDID" "$APP_PATH" - xcrun simctl launch "$SIM_UDID" "$APP_BUNDLE_ID" + echo "Launching with console attached (press Ctrl-C to detach)..." + xcrun simctl launch --console "$SIM_UDID" "$APP_BUNDLE_ID" fi From bf22c8b47b8b6ea7b04c968069c1fd58de220b6d Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 01:22:20 -0700 Subject: [PATCH 2/9] feat(notifications): add TemplateIcon and SystemIcon --- .../Runtime/Notifications/Internal/Notification.cs | 4 ++++ .../Runtime/Notifications/Models/INotification.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs index b7a5007e..1455fb0a 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs @@ -38,11 +38,15 @@ public sealed class ActionButton : IActionButton public string Id => id; public string Text => text; public string Icon => icon; + public string TemplateIcon => templateIcon; + public string SystemIcon => systemIcon; #region Native Field Handling public string id; public string text; public string icon; + public string templateIcon; + public string systemIcon; #endregion } diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs index 98c9a11d..070d4caa 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs @@ -46,6 +46,8 @@ public interface IActionButton string Id { get; } string Text { get; } string Icon { get; } + string TemplateIcon { get; } + string SystemIcon { get; } } /// From bf6c613a5ad95772a72204ee29c5ebbfe4a27e32 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 01:32:53 -0700 Subject: [PATCH 3/9] fix(notifications): null-safe ActionButtons and GroupedNotifications --- .../Runtime/Notifications/Internal/Notification.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs index 1455fb0a..4ae68ca7 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs @@ -125,7 +125,7 @@ public class NotificationBase : INotificationBase /// /// List of action buttons on the notification /// - public List ActionButtons => actionButtons.ToList(); + public List ActionButtons => actionButtons?.ToList(); /// /// Raw JSON payload string received from OneSignal @@ -320,7 +320,7 @@ public class Notification : NotificationBase, INotification /// /// Android only public List GroupedNotifications => - groupedNotifications.ToList(); + groupedNotifications?.ToList(); #region Native Field Handling public List groupedNotifications; From 0bdbda6ea641e0aad98fe4258f15f2850c061d2b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 3 Sep 2026 01:36:29 -0700 Subject: [PATCH 4/9] feat(demo): improve notification click logging --- .../demo/Assets/Scripts/AppBootstrapper.cs | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/examples/demo/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index f87ba8df..fd5d0587 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -1,3 +1,7 @@ +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; using OneSignalDemo.Services; using OneSignalDemo.ViewModels; using OneSignalSDK; @@ -16,6 +20,11 @@ public class AppBootstrapper : MonoBehaviour private const string PlaceholderAppId = "your-onesignal-app-id"; private const string Tag = "AppBootstrapper"; + private static readonly JsonSerializerSettings ClickLogJsonSettings = new() + { + ContractResolver = new PropertiesOnlyContractResolver(), + }; + [SerializeField] private AppViewModel _viewModel; @@ -57,7 +66,7 @@ private async void Start() _apiService.SetAppId(appId); - OneSignal.Debug.LogLevel = LogLevel.Verbose; + OneSignal.Debug.LogLevel = LogLevel.None; #if UNITY_ANDROID && !UNITY_EDITOR SetAndroidWebViewDebugging(false); #endif @@ -179,8 +188,36 @@ private void OnIamDidDismiss(object sender, InAppMessageDidDismissEventArgs e) private void OnIamClicked(object sender, InAppMessageClickEventArgs e) => Debug.Log($"[{Tag}] IAM clicked: {e.Result.ActionId}"); - private void OnNotificationClicked(object sender, NotificationClickEventArgs e) => - Debug.Log($"[{Tag}] Notification clicked: {e.Result.ActionId}"); + private void OnNotificationClicked(object sender, NotificationClickEventArgs e) + { + Debug.Log($"[OneSignal] Notification click: {e.Notification.Title ?? string.Empty}"); + + // to see the full notification click result, uncomment the following code + // var notification = JObject.Parse( + // JsonConvert.SerializeObject( + // e.Notification, + // typeof(INotification), + // Formatting.None, + // ClickLogJsonSettings + // ) + // ); + // notification.Remove("NotifJO"); + + // var click = new JObject + // { + // ["Notification"] = notification, + // ["Result"] = JToken.Parse( + // JsonConvert.SerializeObject( + // e.Result, + // typeof(INotificationClickResult), + // Formatting.None, + // ClickLogJsonSettings + // ) + // ), + // }; + + // LogClickJson(click.ToString(Formatting.Indented)); + } private void OnNotificationForegroundWillDisplay( object sender, @@ -190,5 +227,38 @@ NotificationWillDisplayEventArgs e Debug.Log($"[{Tag}] Notification received in foreground"); e.Notification.Display(); } + + private static void LogClickJson(string json) + { +#if UNITY_ANDROID && !UNITY_EDITOR + const int maxChunkLength = 3000; + using var androidLog = new AndroidJavaClass("android.util.Log"); + + for (var offset = 0; offset < json.Length; ) + { + var length = System.Math.Min(maxChunkLength, json.Length - offset); + if (char.IsHighSurrogate(json[offset + length - 1])) + length--; + + androidLog.CallStatic("i", "OneSignal", json.Substring(offset, length)); + offset += length; + } +#else + Debug.Log(json); +#endif + } + + private sealed class PropertiesOnlyContractResolver : DefaultContractResolver + { + protected override JsonProperty CreateProperty( + MemberInfo member, + MemberSerialization memberSerialization + ) + { + var property = base.CreateProperty(member, memberSerialization); + property.Ignored = member.MemberType != MemberTypes.Property; + return property; + } + } } } From 45c9830b4d505c58a83d8dae02e7707e544417e5 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 9 Sep 2026 17:28:20 -0700 Subject: [PATCH 5/9] feat(notifications): change RawPayload type to IDictionary --- .../Runtime/AndroidDisplayableNotification.cs | 2 +- .../Runtime/AndroidNotificationsManager.cs | 3 +- .../Notifications/Internal/Notification.cs | 6 +- .../Notifications/Models/INotification.cs | 4 +- .../Runtime/iOSNotificationsManager.cs | 2 +- .../demo/Assets/Scripts/AppBootstrapper.cs | 79 +++++++++++-------- 6 files changed, 56 insertions(+), 40 deletions(-) diff --git a/com.onesignal.unitysdk.android/Runtime/AndroidDisplayableNotification.cs b/com.onesignal.unitysdk.android/Runtime/AndroidDisplayableNotification.cs index 1108e7cc..121ab3f1 100644 --- a/com.onesignal.unitysdk.android/Runtime/AndroidDisplayableNotification.cs +++ b/com.onesignal.unitysdk.android/Runtime/AndroidDisplayableNotification.cs @@ -34,7 +34,7 @@ namespace OneSignalSDK.Android.Notifications.Models { public sealed class AndroidDisplayableNotification : Notification, IDisplayableNotification { - public AndroidJavaObject NotifJO { get; set; } + internal AndroidJavaObject NotifJO { get; set; } public void Display() => NotifJO?.Call("display"); } diff --git a/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs b/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs index fe1f99ea..d77e6e0d 100644 --- a/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs +++ b/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs @@ -279,7 +279,8 @@ private static AndroidDisplayableNotification _getNotification(AndroidJavaObject if (rawPayloadJson != null) { var rawPayloadJsonStr = rawPayloadJson.Call("toString"); - notification.rawPayload = rawPayloadJsonStr; + notification.rawPayload = + Json.Deserialize(rawPayloadJsonStr) as Dictionary; } // attach the Java-Object to the notifification just built. diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs index 4ae68ca7..1023de47 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs @@ -128,9 +128,9 @@ public class NotificationBase : INotificationBase public List ActionButtons => actionButtons?.ToList(); /// - /// Raw JSON payload string received from OneSignal + /// Raw payload received from OneSignal /// - public string RawPayload => rawPayload; + public IDictionary RawPayload => rawPayload; #region Android /// @@ -286,7 +286,7 @@ public class NotificationBase : INotificationBase public string collapseId; public IDictionary additionalData; public List actionButtons; - public string rawPayload; + public IDictionary rawPayload; public int androidNotificationId; public string smallIcon; public string largeIcon; diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs index 070d4caa..798f6932 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs @@ -120,9 +120,9 @@ public interface INotificationBase List ActionButtons { get; } /// - /// Raw JSON payload string received from OneSignal + /// Raw payload received from OneSignal /// - string RawPayload { get; } + IDictionary RawPayload { get; } #region Android /// diff --git a/com.onesignal.unitysdk.ios/Runtime/iOSNotificationsManager.cs b/com.onesignal.unitysdk.ios/Runtime/iOSNotificationsManager.cs index 265bcb63..fd210717 100644 --- a/com.onesignal.unitysdk.ios/Runtime/iOSNotificationsManager.cs +++ b/com.onesignal.unitysdk.ios/Runtime/iOSNotificationsManager.cs @@ -263,7 +263,7 @@ private static void _fillNotifFromObj(ref iOSDisplayableNotification notif, obje notifDict.ContainsKey("rawPayload") && notifDict["rawPayload"] is Dictionary payloadDict ) - notif.rawPayload = Json.Serialize(payloadDict); + notif.rawPayload = payloadDict; } [AOT.MonoPInvokeCallback(typeof(BooleanResponseDelegate))] diff --git a/examples/demo/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index fd5d0587..ac048429 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -1,6 +1,6 @@ +using System.Collections; using System.Reflection; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using OneSignalDemo.Services; using OneSignalDemo.ViewModels; @@ -20,9 +20,11 @@ public class AppBootstrapper : MonoBehaviour private const string PlaceholderAppId = "your-onesignal-app-id"; private const string Tag = "AppBootstrapper"; - private static readonly JsonSerializerSettings ClickLogJsonSettings = new() + private static readonly JsonSerializerSettings EventLogJsonSettings = new() { - ContractResolver = new PropertiesOnlyContractResolver(), + ContractResolver = new PropertiesOnlyCamelCaseContractResolver(), + DefaultValueHandling = DefaultValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, }; [SerializeField] @@ -192,31 +194,8 @@ private void OnNotificationClicked(object sender, NotificationClickEventArgs e) { Debug.Log($"[OneSignal] Notification click: {e.Notification.Title ?? string.Empty}"); - // to see the full notification click result, uncomment the following code - // var notification = JObject.Parse( - // JsonConvert.SerializeObject( - // e.Notification, - // typeof(INotification), - // Formatting.None, - // ClickLogJsonSettings - // ) - // ); - // notification.Remove("NotifJO"); - - // var click = new JObject - // { - // ["Notification"] = notification, - // ["Result"] = JToken.Parse( - // JsonConvert.SerializeObject( - // e.Result, - // typeof(INotificationClickResult), - // Formatting.None, - // ClickLogJsonSettings - // ) - // ), - // }; - - // LogClickJson(click.ToString(Formatting.Indented)); + // uncomment to see the full event object + LogJson("[OneSignal] click event:", e); } private void OnNotificationForegroundWillDisplay( @@ -224,12 +203,47 @@ private void OnNotificationForegroundWillDisplay( NotificationWillDisplayEventArgs e ) { - Debug.Log($"[{Tag}] Notification received in foreground"); - e.Notification.Display(); + Debug.Log( + $"[OneSignal] Notification foregroundWillDisplay: {e.Notification.Title ?? string.Empty}" + ); + + // uncomment to see the full notification object + LogJson("[OneSignal] will display event:", e.Notification); + + // uncomment to test preventing the default display behavior + // e.PreventDefault(); + + // call this after PreventDefault() (within about 25 seconds) to force display + // e.Notification.Display(); + + // example with a delay (assumes PreventDefault() was called) + // StartCoroutine(DisplayNotificationAfterDelay(e.Notification, 24)); + } + + private static IEnumerator DisplayNotificationAfterDelay( + IDisplayableNotification notification, + int seconds + ) + { + Debug.Log($"[OneSignal] Forcing notification display in {seconds} seconds"); + + while (seconds > 0) + { + Debug.Log($"[OneSignal] Displaying notification in {seconds} seconds"); + yield return new WaitForSecondsRealtime(1); + seconds--; + } + + Debug.Log("[OneSignal] Displaying notification"); + notification.Display(); } - private static void LogClickJson(string json) + private static void LogJson(string label, object value) { + var json = + $"{label}\n" + + JsonConvert.SerializeObject(value, Formatting.Indented, EventLogJsonSettings); + #if UNITY_ANDROID && !UNITY_EDITOR const int maxChunkLength = 3000; using var androidLog = new AndroidJavaClass("android.util.Log"); @@ -248,7 +262,8 @@ private static void LogClickJson(string json) #endif } - private sealed class PropertiesOnlyContractResolver : DefaultContractResolver + private sealed class PropertiesOnlyCamelCaseContractResolver + : CamelCasePropertyNamesContractResolver { protected override JsonProperty CreateProperty( MemberInfo member, From 69f111ed7636eabb0e309b62eded5a80d2443854 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 9 Sep 2026 17:38:00 -0700 Subject: [PATCH 6/9] refactor(notifications): properly deserialize grouped notifications --- .../Runtime/AndroidNotificationsManager.cs | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs b/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs index d77e6e0d..bcb2a428 100644 --- a/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs +++ b/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs @@ -256,37 +256,62 @@ public void onClick(AndroidJavaObject clickEvent) private static AndroidDisplayableNotification _getNotification(AndroidJavaObject notifJO) { var notification = notifJO.ToSerializable(); - - var dataJson = notifJO.Call("getAdditionalData"); - if (dataJson != null) - { - var dataJsonStr = dataJson.Call("toString"); - notification.additionalData = - Json.Deserialize(dataJsonStr) as Dictionary; - } + _fillNotificationDictionaries(notification, notifJO); var groupedNotificationsJson = notifJO.Call( "getGroupedNotifications" ); if (groupedNotificationsJson != null) + notification.groupedNotifications = _getGroupedNotifications( + groupedNotificationsJson + ); + + // attach the Java-Object to the notifification just built. + notification.NotifJO = notifJO; + + return notification; + } + + private static List _getGroupedNotifications( + AndroidJavaObject groupedNotifications + ) + { + var count = groupedNotifications.Call("size"); + var notifications = new List(count); + for (var index = 0; index < count; index++) { - var groupedNotificationsStr = groupedNotificationsJson.Call("toString"); - notification.groupedNotifications = - Json.Deserialize(groupedNotificationsStr) as List; + using var notificationJO = groupedNotifications.Call( + "get", + index + ); + var notification = notificationJO.ToSerializable(); + _fillNotificationDictionaries(notification, notificationJO); + notifications.Add(notification); + } + + return notifications; + } + + private static void _fillNotificationDictionaries( + NotificationBase notification, + AndroidJavaObject notificationJO + ) + { + var dataJson = notificationJO.Call("getAdditionalData"); + if (dataJson != null) + { + var dataJsonStr = dataJson.Call("toString"); + notification.additionalData = + Json.Deserialize(dataJsonStr) as Dictionary; } - var rawPayloadJson = notifJO.Call("getRawPayload"); + var rawPayloadJson = notificationJO.Call("getRawPayload"); if (rawPayloadJson != null) { var rawPayloadJsonStr = rawPayloadJson.Call("toString"); notification.rawPayload = Json.Deserialize(rawPayloadJsonStr) as Dictionary; } - - // attach the Java-Object to the notifification just built. - notification.NotifJO = notifJO; - - return notification; } } } From 7afbc3830ceb25c908506881849008ee2bbadcd6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 9 Sep 2026 17:47:47 -0700 Subject: [PATCH 7/9] chore(demo): comment out verbose event logging --- examples/demo/Assets/Scripts/AppBootstrapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/demo/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index ac048429..3c1bba80 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -195,7 +195,7 @@ private void OnNotificationClicked(object sender, NotificationClickEventArgs e) Debug.Log($"[OneSignal] Notification click: {e.Notification.Title ?? string.Empty}"); // uncomment to see the full event object - LogJson("[OneSignal] click event:", e); + // LogJson("[OneSignal] click event:", e); } private void OnNotificationForegroundWillDisplay( @@ -208,7 +208,7 @@ NotificationWillDisplayEventArgs e ); // uncomment to see the full notification object - LogJson("[OneSignal] will display event:", e.Notification); + // LogJson("[OneSignal] will display event:", e.Notification); // uncomment to test preventing the default display behavior // e.PreventDefault(); From 5c6f5c251bb471a8825606c8333012a9b932a6ce Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 10 Sep 2026 11:36:07 -0700 Subject: [PATCH 8/9] docs(notifications): add XML docs to icon properties --- .../Runtime/Notifications/Models/INotification.cs | 10 ++++++++++ examples/demo/Assets/Scripts/AppBootstrapper.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs index 798f6932..2ba48bf9 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs @@ -46,7 +46,17 @@ public interface IActionButton string Id { get; } string Text { get; } string Icon { get; } + + /// + /// Template image name for the action button icon + /// + /// iOS only string TemplateIcon { get; } + + /// + /// SF Symbol name for the action button icon + /// + /// iOS only string SystemIcon { get; } } diff --git a/examples/demo/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index 3c1bba80..2e0e08ba 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -68,7 +68,7 @@ private async void Start() _apiService.SetAppId(appId); - OneSignal.Debug.LogLevel = LogLevel.None; + OneSignal.Debug.LogLevel = LogLevel.Verbose; #if UNITY_ANDROID && !UNITY_EDITOR SetAndroidWebViewDebugging(false); #endif From 70719ca12707cf9361a0ba52dfac4b55200122e9 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 10 Sep 2026 12:03:44 -0700 Subject: [PATCH 9/9] chore: [SDK-5161] fix v6 formatting Co-authored-by: Cursor --- com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs b/com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs index 97c71563..4f945f7e 100644 --- a/com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs +++ b/com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs @@ -104,8 +104,7 @@ private static Settings Load() try { var values = - Json.Deserialize(File.ReadAllText(_settingsPath)) - as Dictionary; + Json.Deserialize(File.ReadAllText(_settingsPath)) as Dictionary; if ( values != null && values.TryGetValue(nameof(Settings.disableLocation), out var value)