diff --git a/com.onesignal.unitysdk.android/Runtime/AndroidDisplayableNotification.cs b/com.onesignal.unitysdk.android/Runtime/AndroidDisplayableNotification.cs index 1108e7cc8..121ab3f14 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 fe1f99ea1..bcb2a4282 100644 --- a/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs +++ b/com.onesignal.unitysdk.android/Runtime/AndroidNotificationsManager.cs @@ -256,36 +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 = rawPayloadJsonStr; + notification.rawPayload = + Json.Deserialize(rawPayloadJsonStr) as Dictionary; } - - // attach the Java-Object to the notifification just built. - notification.NotifJO = notifJO; - - return notification; } } } diff --git a/com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs b/com.onesignal.unitysdk.core/Editor/OneSignalSDKSettings.cs index 97c715635..4f945f7e2 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) diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Internal/Notification.cs index b7a5007e4..1023de47a 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 } @@ -121,12 +125,12 @@ 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 + /// Raw payload received from OneSignal /// - public string RawPayload => rawPayload; + public IDictionary RawPayload => rawPayload; #region Android /// @@ -282,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; @@ -316,7 +320,7 @@ public class Notification : NotificationBase, INotification /// /// Android only public List GroupedNotifications => - groupedNotifications.ToList(); + groupedNotifications?.ToList(); #region Native Field Handling public List groupedNotifications; diff --git a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs index 98c9a11dd..2ba48bf90 100755 --- a/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs +++ b/com.onesignal.unitysdk.core/Runtime/Notifications/Models/INotification.cs @@ -46,6 +46,18 @@ 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; } } /// @@ -118,9 +130,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 265bcb638..fd2107175 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/App/Editor/iOS/BuildPostProcessor.cs b/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs index e1ecfeb47..ee1212930 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/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index f87ba8df1..2e0e08baf 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -1,3 +1,7 @@ +using System.Collections; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using OneSignalDemo.Services; using OneSignalDemo.ViewModels; using OneSignalSDK; @@ -16,6 +20,13 @@ public class AppBootstrapper : MonoBehaviour private const string PlaceholderAppId = "your-onesignal-app-id"; private const string Tag = "AppBootstrapper"; + private static readonly JsonSerializerSettings EventLogJsonSettings = new() + { + ContractResolver = new PropertiesOnlyCamelCaseContractResolver(), + DefaultValueHandling = DefaultValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + }; + [SerializeField] private AppViewModel _viewModel; @@ -179,16 +190,90 @@ 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}"); + + // uncomment to see the full event object + // LogJson("[OneSignal] click event:", e); + } private void OnNotificationForegroundWillDisplay( object sender, 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 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"); + + 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 PropertiesOnlyCamelCaseContractResolver + : CamelCasePropertyNamesContractResolver + { + protected override JsonProperty CreateProperty( + MemberInfo member, + MemberSerialization memberSerialization + ) + { + var property = base.CreateProperty(member, memberSerialization); + property.Ignored = member.MemberType != MemberTypes.Property; + return property; + } } } } diff --git a/examples/demo/README.md b/examples/demo/README.md index a4e288c0c..034753284 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 000000000..99281835a Binary files /dev/null and b/examples/demo/iOS/NotificationIcons/template-bookmark-icon.png differ diff --git a/examples/demo/iOS/NotificationIcons/template-share-icon.png b/examples/demo/iOS/NotificationIcons/template-share-icon.png new file mode 100644 index 000000000..2457e1878 Binary files /dev/null and b/examples/demo/iOS/NotificationIcons/template-share-icon.png differ diff --git a/examples/demo/run-android.sh b/examples/demo/run-android.sh index 670a4442f..f23d1e288 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 835e6c3f0..228e9b517 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