From c6ebd90ecaba3ea689e3b9e5e4b9ccf09db4a7df Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 23 Oct 2026 15:23:37 -0700 Subject: [PATCH] chore: [SDK-5531] add sticky log view to demo app Co-Authored-By: Claude Opus 5.5 --- examples/demo/Assets/Resources/Theme.uss | 87 ++++++++++++ .../demo/Assets/Scripts/AppBootstrapper.cs | 18 ++- .../Assets/Scripts/Services/LogService.cs | 58 ++++++++ .../Scripts/Services/LogService.cs.meta | 2 + .../Assets/Scripts/UI/HomeScreenController.cs | 2 + examples/demo/Assets/Scripts/UI/LogView.cs | 131 ++++++++++++++++++ .../demo/Assets/Scripts/UI/LogView.cs.meta | 2 + .../Assets/Scripts/ViewModels/AppViewModel.cs | 21 ++- 8 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 examples/demo/Assets/Scripts/Services/LogService.cs create mode 100644 examples/demo/Assets/Scripts/Services/LogService.cs.meta create mode 100644 examples/demo/Assets/Scripts/UI/LogView.cs create mode 100644 examples/demo/Assets/Scripts/UI/LogView.cs.meta diff --git a/examples/demo/Assets/Resources/Theme.uss b/examples/demo/Assets/Resources/Theme.uss index a7e7fced..374b2f48 100644 --- a/examples/demo/Assets/Resources/Theme.uss +++ b/examples/demo/Assets/Resources/Theme.uss @@ -13,6 +13,8 @@ --os-text-primary: #202124; --os-text-secondary: #757575; --os-text-hint: #9E9E9E; + --os-log-background: #1E1E1E; + --os-log-text: #D4D4D4; --card-gap: 8px; } @@ -170,6 +172,91 @@ flex-shrink: 0; } +.log-view { + background-color: var(--os-log-background); + flex-shrink: 0; +} + +.log-header { + flex-direction: row; + align-items: center; + height: 24px; + padding: 0 16px; +} + +.log-header-label { + flex-grow: 1; + padding: 0; + margin: 0; + font-size: 11px; + -unity-font-style: bold; + letter-spacing: 0.5px; + color: var(--os-grey-500); + -unity-font-definition: resource("DroidSansMono"); +} + +.log-header-button { + width: 24px; + height: 24px; + padding: 0; + margin: 0; + border-width: 0; + border-radius: 0; + background-color: rgba(0, 0, 0, 0); + color: var(--os-grey-500); + -unity-font-definition: resource("MaterialIcons-Regular"); + -unity-text-align: middle-center; +} + +.log-clear-button { + font-size: 14px; +} + +/* 24px hit boxes; margins keep glyphs 12px apart and flush with the 16px padding. */ +.log-toggle-button { + font-size: 18px; + margin: 0 -3px 0 4px; +} + +.log-body { + height: 100px; +} + +.log-body-content { + padding: 0 16px 6px 16px; +} + +.log-entry { + flex-direction: row; + align-items: flex-start; +} + +.log-entry-time, +.log-entry-message, +.log-empty { + font-size: 8px; + padding: 0; + margin: 0; + -unity-font-definition: resource("DroidSansMono"); +} + +.log-entry-time, +.log-empty { + color: var(--os-grey-600); +} + +.log-entry-time { + margin-right: 5px; + flex-shrink: 0; +} + +.log-entry-message { + flex-shrink: 1; + flex-grow: 1; + white-space: normal; + color: var(--os-log-text); +} + .section-container { } diff --git a/examples/demo/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index f87ba8df..7b9e5a97 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -159,35 +159,39 @@ private void OnIamWillDisplay(object sender, InAppMessageWillDisplayEventArgs e) #if UNITY_ANDROID && !UNITY_EDITOR SetAndroidWebViewDebugging(true); #endif - Debug.Log($"[{Tag}] IAM will display: {e.Message.MessageId}"); + LogService.Show($"IAM willDisplay: {e.Message.MessageId}"); } private void OnIamDidDisplay(object sender, InAppMessageDidDisplayEventArgs e) => - Debug.Log($"[{Tag}] IAM did display: {e.Message.MessageId}"); + LogService.Show($"IAM didDisplay: {e.Message.MessageId}"); private void OnIamWillDismiss(object sender, InAppMessageWillDismissEventArgs e) => - Debug.Log($"[{Tag}] IAM will dismiss: {e.Message.MessageId}"); + LogService.Show($"IAM willDismiss: {e.Message.MessageId}"); private void OnIamDidDismiss(object sender, InAppMessageDidDismissEventArgs e) { - Debug.Log($"[{Tag}] IAM did dismiss: {e.Message.MessageId}"); + LogService.Show($"IAM didDismiss: {e.Message.MessageId}"); #if UNITY_ANDROID && !UNITY_EDITOR SetAndroidWebViewDebugging(false); #endif } private void OnIamClicked(object sender, InAppMessageClickEventArgs e) => - Debug.Log($"[{Tag}] IAM clicked: {e.Result.ActionId}"); + LogService.Show($"IAM click: {e.Message.MessageId}"); private void OnNotificationClicked(object sender, NotificationClickEventArgs e) => - Debug.Log($"[{Tag}] Notification clicked: {e.Result.ActionId}"); + LogService.Show( + $"Notification click: {e.Notification.NotificationId ?? "null"} {e.Notification.Title ?? ""}" + ); private void OnNotificationForegroundWillDisplay( object sender, NotificationWillDisplayEventArgs e ) { - Debug.Log($"[{Tag}] Notification received in foreground"); + LogService.Show( + $"Notification foregroundWillDisplay: {e.Notification.NotificationId ?? "null"} {e.Notification.Title ?? ""}" + ); e.Notification.Display(); } } diff --git a/examples/demo/Assets/Scripts/Services/LogService.cs b/examples/demo/Assets/Scripts/Services/LogService.cs new file mode 100644 index 00000000..8feb47c1 --- /dev/null +++ b/examples/demo/Assets/Scripts/Services/LogService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace OneSignalDemo.Services +{ + public readonly struct LogEntry + { + public readonly string Time; + public readonly string Message; + + public LogEntry(string time, string message) + { + Time = time; + Message = message; + } + } + + public static class LogService + { + private const int MaxEntries = 100; + + // In memory only, so each launch starts empty and the Appium suite can assert per-launch counts. + private static readonly List _entries = new(); + + public static IReadOnlyList Entries => _entries; + + public static event Action Changed; + + public static void Info(string message) => Debug.Log($"[OneSignal] {message}"); + + public static void Show(string message) + { + Info(message); + AddToView(message); + } + + public static void Error(string message) + { + Debug.LogError($"[OneSignal] {message}"); + AddToView(message); + } + + public static void Clear() + { + _entries.Clear(); + Changed?.Invoke(); + } + + private static void AddToView(string message) + { + _entries.Add(new LogEntry(DateTime.Now.ToString("HH:mm:ss"), message)); + if (_entries.Count > MaxEntries) + _entries.RemoveRange(0, _entries.Count - MaxEntries); + Changed?.Invoke(); + } + } +} diff --git a/examples/demo/Assets/Scripts/Services/LogService.cs.meta b/examples/demo/Assets/Scripts/Services/LogService.cs.meta new file mode 100644 index 00000000..4e3b423b --- /dev/null +++ b/examples/demo/Assets/Scripts/Services/LogService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 138cf52534c848a1ac384ab565a4bfaf \ No newline at end of file diff --git a/examples/demo/Assets/Scripts/UI/HomeScreenController.cs b/examples/demo/Assets/Scripts/UI/HomeScreenController.cs index fb9a4fa1..f6d0061c 100644 --- a/examples/demo/Assets/Scripts/UI/HomeScreenController.cs +++ b/examples/demo/Assets/Scripts/UI/HomeScreenController.cs @@ -144,6 +144,8 @@ private void BuildScreen() screenRoot.Add(appBar); + screenRoot.Add(new LogView().Root); + var appBarShadow = new VisualElement(); appBarShadow.AddToClassList("app-bar-shadow"); screenRoot.Add(appBarShadow); diff --git a/examples/demo/Assets/Scripts/UI/LogView.cs b/examples/demo/Assets/Scripts/UI/LogView.cs new file mode 100644 index 00000000..5954e67a --- /dev/null +++ b/examples/demo/Assets/Scripts/UI/LogView.cs @@ -0,0 +1,131 @@ +using OneSignalDemo.Services; +using UnityEngine; +using UnityEngine.UIElements; + +namespace OneSignalDemo.UI +{ + public class LogView + { + public VisualElement Root { get; } + + private readonly Label _headerLabel; + private readonly Button _toggleButton; + private readonly ScrollView _body; + private bool _collapsed; + + public LogView() + { + Root = new VisualElement(); + Root.name = "log_view"; + Root.AddToClassList("log-view"); + + var header = new VisualElement(); + header.AddToClassList("log-header"); + + _headerLabel = new Label(); + _headerLabel.AddToClassList("log-header-label"); + header.Add(_headerLabel); + + var clearButton = new Button(LogService.Clear); + clearButton.name = "log_clear_button"; + clearButton.text = MaterialIcons.Delete; + clearButton.AddToClassList("log-header-button"); + clearButton.AddToClassList("log-clear-button"); + AccessibilityBridge.RegisterE2ETapTarget(clearButton, () => true, LogService.Clear); + header.Add(clearButton); + + _toggleButton = new Button(ToggleCollapsed); + _toggleButton.name = "log_toggle_button"; + _toggleButton.AddToClassList("log-header-button"); + _toggleButton.AddToClassList("log-toggle-button"); + AccessibilityBridge.RegisterE2ETapTarget(_toggleButton, () => true, ToggleCollapsed); + header.Add(_toggleButton); + + Root.Add(header); + + _body = new ScrollView(ScrollViewMode.Vertical); + _body.AddToClassList("log-body"); + _body.contentContainer.AddToClassList("log-body-content"); + _body.verticalScrollerVisibility = ScrollerVisibility.Hidden; + _body.horizontalScrollerVisibility = ScrollerVisibility.Hidden; + _body.touchScrollBehavior = ScrollView.TouchScrollBehavior.Clamped; + _body.contentContainer.RegisterCallback(_ => ScrollToEnd()); + Root.Add(_body); + + // Entries is static, so only stay subscribed while attached or rebuilt screens leak. + Root.RegisterCallback(_ => + { + LogService.Changed += Render; + Render(); + }); + Root.RegisterCallback(_ => LogService.Changed -= Render); + + ApplyCollapsed(); + } + + private void ToggleCollapsed() + { + _collapsed = !_collapsed; + ApplyCollapsed(); + AccessibilityBridge.RequestResync(); + } + + private void ApplyCollapsed() + { + _body.style.display = _collapsed ? DisplayStyle.None : DisplayStyle.Flex; + _toggleButton.text = _collapsed ? MaterialIcons.ExpandMore : MaterialIcons.ExpandLess; + if (!_collapsed) + ScrollToEnd(); + } + + // Rebuilt in full so `log_entry_{index}` stays 0-based after the oldest entries are dropped. + private void Render() + { + var entries = LogService.Entries; + _headerLabel.text = $"LOGS ({entries.Count})"; + _body.Clear(); + + if (entries.Count == 0) + { + var empty = new Label("No logs yet"); + empty.AddToClassList("log-empty"); + _body.Add(empty); + } + + for (var i = 0; i < entries.Count; i++) + { + var row = new VisualElement(); + row.AddToClassList("log-entry"); + + var time = new Label(entries[i].Time); + time.AddToClassList("log-entry-time"); + row.Add(time); + + // Plain text (no rich-text spans) so the a11y bridge reports the message verbatim. + var message = new Label(entries[i].Message); + message.name = $"log_entry_{i}"; + message.enableRichText = false; + message.AddToClassList("log-entry-message"); + row.Add(message); + + _body.Add(row); + } + + AccessibilityBridge.RequestResync(); + } + + // Deferred a tick so the ScrollView has picked up the new content height. + private void ScrollToEnd() + { + if (_collapsed) + return; + _body.schedule.Execute(() => + { + var overflow = + _body.contentContainer.layout.height - _body.contentViewport.layout.height; + if (overflow > 0) + _body.scrollOffset = new Vector2(0, overflow); + }); + } + } +} diff --git a/examples/demo/Assets/Scripts/UI/LogView.cs.meta b/examples/demo/Assets/Scripts/UI/LogView.cs.meta new file mode 100644 index 00000000..ab79c785 --- /dev/null +++ b/examples/demo/Assets/Scripts/UI/LogView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e71f90db01984bacb336c4cb8111394e \ No newline at end of file diff --git a/examples/demo/Assets/Scripts/ViewModels/AppViewModel.cs b/examples/demo/Assets/Scripts/ViewModels/AppViewModel.cs index 21df63a5..a2dce9da 100644 --- a/examples/demo/Assets/Scripts/ViewModels/AppViewModel.cs +++ b/examples/demo/Assets/Scripts/ViewModels/AppViewModel.cs @@ -606,6 +606,14 @@ private void SetLoading(bool loading) private void NotifyStateChanged() => OnStateChanged?.Invoke(); + private static string NullIfEmpty(string value) => + string.IsNullOrEmpty(value) ? "null" : value; + + private static string FormatBool(bool value) => value ? "true" : "false"; + + private static string FormatToken(string value) => + string.IsNullOrEmpty(value) ? "null" : $"{value.Substring(0, Math.Min(8, value.Length))}…"; + private static void MergePairs( List> target, IDictionary source @@ -639,20 +647,27 @@ private void OnPushSubscriptionChanged(object sender, PushSubscriptionChangedEve { _pushSubscriptionId = OneSignal.User.PushSubscription.Id ?? ""; _pushOptedIn = OneSignal.User.PushSubscription.OptedIn; - Debug.Log($"[{Tag}] Push subscription changed: {_pushSubscriptionId}"); + var previous = e.State.Previous; + var current = e.State.Current; + LogService.Show( + $"Push subscription changed: id={NullIfEmpty(previous.Id)} → {NullIfEmpty(current.Id)}, optedIn={FormatBool(previous.OptedIn)} → {FormatBool(current.OptedIn)}, token={FormatToken(previous.Token)} → {FormatToken(current.Token)}" + ); NotifyStateChanged(); } private void OnPermissionChanged(object sender, NotificationPermissionChangedEventArgs e) { _hasPermission = e.Permission; - Debug.Log($"[{Tag}] Permission changed: {e.Permission}"); + LogService.Show($"Permission changed: {FormatBool(e.Permission)}"); NotifyStateChanged(); } private async void OnUserChanged(object sender, UserStateChangedEventArgs e) { - Debug.Log($"[{Tag}] User changed, fetching data..."); + var current = e.State.Current; + LogService.Show( + $"User changed: onesignalId={NullIfEmpty(current.OneSignalId)}, externalId={NullIfEmpty(current.ExternalId)}" + ); _oneSignalId = OneSignal.User.OneSignalId ?? ""; NotifyStateChanged(); await FetchUserDataFromApi();