diff --git a/com.onesignal.unity.android/Runtime/AndroidLiveActivitiesManager.cs b/com.onesignal.unity.android/Runtime/AndroidLiveActivitiesManager.cs index 806e6ab72..1787d17e8 100644 --- a/com.onesignal.unity.android/Runtime/AndroidLiveActivitiesManager.cs +++ b/com.onesignal.unity.android/Runtime/AndroidLiveActivitiesManager.cs @@ -40,6 +40,7 @@ public Task EnterAsync(string activityId, string token) return Task.FromResult(false); } + [System.Obsolete("Currently unsupported, avoid using this method.")] public Task ExitAsync(string activityId) { SDKDebug.Warn("This feature is only available for iOS."); diff --git a/com.onesignal.unity.core/Editor/Platform/LiveActivitiesManager.cs b/com.onesignal.unity.core/Editor/Platform/LiveActivitiesManager.cs index f51b896cb..ff0465c33 100644 --- a/com.onesignal.unity.core/Editor/Platform/LiveActivitiesManager.cs +++ b/com.onesignal.unity.core/Editor/Platform/LiveActivitiesManager.cs @@ -37,6 +37,7 @@ public Task EnterAsync(string activityId, string token) return Task.FromResult(false); } + [System.Obsolete("Currently unsupported, avoid using this method.")] public Task ExitAsync(string activityId) { return Task.FromResult(false); diff --git a/com.onesignal.unity.core/Runtime/LiveActivities/ILiveActivitiesManager.cs b/com.onesignal.unity.core/Runtime/LiveActivities/ILiveActivitiesManager.cs index 99191bc7e..a39f200fb 100644 --- a/com.onesignal.unity.core/Runtime/LiveActivities/ILiveActivitiesManager.cs +++ b/com.onesignal.unity.core/Runtime/LiveActivities/ILiveActivitiesManager.cs @@ -45,6 +45,7 @@ public interface ILiveActivitiesManager /// /// iOS Only /// Awaitable boolean of whether the operation succeeded or failed + [System.Obsolete("Currently unsupported, avoid using this method.")] Task ExitAsync(string activityId); /// diff --git a/com.onesignal.unity.ios/Runtime/iOSLiveActivitiesManager.cs b/com.onesignal.unity.ios/Runtime/iOSLiveActivitiesManager.cs index 2d0d47b3d..7562942f9 100644 --- a/com.onesignal.unity.ios/Runtime/iOSLiveActivitiesManager.cs +++ b/com.onesignal.unity.ios/Runtime/iOSLiveActivitiesManager.cs @@ -75,6 +75,7 @@ public async Task EnterAsync(string activityId, string token) return await proxy; } + [System.Obsolete("Currently unsupported, avoid using this method.")] public async Task ExitAsync(string activityId) { var (proxy, hashCode) = WaitingProxy._setupProxy(); diff --git a/examples/demo/.env.example b/examples/demo/.env.example new file mode 100644 index 000000000..674a938f9 --- /dev/null +++ b/examples/demo/.env.example @@ -0,0 +1 @@ +ONESIGNAL_API_KEY=your_rest_api_key diff --git a/examples/demo/.gitignore b/examples/demo/.gitignore index 8ec7575ff..c737feee4 100644 --- a/examples/demo/.gitignore +++ b/examples/demo/.gitignore @@ -64,6 +64,11 @@ crashlytics-build.properties # User-specific Unity Editor settings /[Uu]serSettings/ +# Environment files +.env +Assets/StreamingAssets/.env +Assets/StreamingAssets/.env.meta + # Gradle template backup files *.backup *.backup.meta diff --git a/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs b/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs index 87662db2c..1beb3d097 100644 --- a/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs +++ b/examples/demo/Assets/App/Editor/iOS/BuildPostProcessor.cs @@ -38,19 +38,19 @@ namespace App.Editor.iOS { /// - /// Adds the ExampleWidgetExtension to the iOS project frameworks to the iOS project and enables the main target + /// Adds the OneSignalWidgetExtension to the iOS project and enables the main target /// for Live Activities. /// public class BuildPostProcessor : IPostprocessBuildWithReport { - private static readonly string WdigetExtensionTargetRelativePath = "ExampleWidget"; - private static readonly string WidgetExtensionTargetName = "ExampleWidgetExtension"; - private static readonly string WidgetExtensionPath = Path.Combine("iOS", "ExampleWidget"); + private static readonly string WidgetExtensionTargetRelativePath = "OneSignalWidget"; + private static readonly string WidgetExtensionTargetName = "OneSignalWidgetExtension"; + private static readonly string WidgetExtensionPath = Path.Combine("iOS", "OneSignalWidget"); private static readonly string[] WidgetExtensionFiles = new string[] { "Assets.xcassets", - "ExampleWidgetBundle.swift", - "ExampleWidgetLiveActivity.swift", + "OneSignalWidgetBundle.swift", + "OneSignalWidgetLiveActivity.swift", }; /// @@ -114,7 +114,7 @@ static void AddWidgetExtensionToProject(string outputPath) if (!string.IsNullOrEmpty(extensionGuid)) return; - var widgetDestPath = Path.Combine(outputPath, WdigetExtensionTargetRelativePath); + var widgetDestPath = Path.Combine(outputPath, WidgetExtensionTargetRelativePath); Directory.CreateDirectory(widgetDestPath); CopyFileOrDirectory( @@ -126,14 +126,14 @@ static void AddWidgetExtensionToProject(string outputPath) project.GetUnityMainTargetGuid(), WidgetExtensionTargetName, $"{PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.iOS)}.{WidgetExtensionTargetName}", - $"{WdigetExtensionTargetRelativePath}/Info.plist" + $"{WidgetExtensionTargetRelativePath}/Info.plist" ); var buildPhaseID = project.AddSourcesBuildPhase(extensionGuid); foreach (var file in WidgetExtensionFiles) { - var destPathRelative = Path.Combine(WdigetExtensionTargetRelativePath, file); + var destPathRelative = Path.Combine(WidgetExtensionTargetRelativePath, file); var sourceFileGuid = project.AddFile(destPathRelative, destPathRelative); project.AddFileToBuildSection(extensionGuid, buildPhaseID, sourceFileGuid); CopyFileOrDirectory( diff --git a/examples/demo/Assets/Resources/Theme.uss b/examples/demo/Assets/Resources/Theme.uss index 2722be03c..b03402fab 100644 --- a/examples/demo/Assets/Resources/Theme.uss +++ b/examples/demo/Assets/Resources/Theme.uss @@ -765,6 +765,14 @@ Scroller { max-height: 0; } +.inline-input-field > .unity-text-field__input { + background-color: transparent; + border-width: 0; + -unity-text-align: upper-right; + color: var(--os-text-primary); + font-size: 14px; +} + .unity-base-slider { display: none; } diff --git a/examples/demo/Assets/Scripts/AppBootstrapper.cs b/examples/demo/Assets/Scripts/AppBootstrapper.cs index 77b6cb06b..71f68141c 100644 --- a/examples/demo/Assets/Scripts/AppBootstrapper.cs +++ b/examples/demo/Assets/Scripts/AppBootstrapper.cs @@ -4,6 +4,7 @@ using OneSignalSDK; using OneSignalSDK.Debug.Models; using OneSignalSDK.InAppMessages; +using OneSignalSDK.LiveActivities; using OneSignalSDK.Notifications; using UnityEngine; @@ -40,12 +41,23 @@ private async void Start() } _apiService.SetAppId(appId); + _apiService.LoadApiKey(); OneSignal.Debug.LogLevel = LogLevel.Verbose; OneSignal.ConsentRequired = _prefs.ConsentRequired; OneSignal.ConsentGiven = _prefs.PrivacyConsent; OneSignal.Initialize(appId); +#if UNITY_IOS + OneSignal.LiveActivities.SetupDefault( + new LiveActivitySetupOptions + { + EnablePushToStart = true, + EnablePushToUpdate = true, + } + ); +#endif + OneSignal.InAppMessages.Paused = _prefs.IamPaused; OneSignal.Location.IsShared = _prefs.LocationShared; diff --git a/examples/demo/Assets/Scripts/Editor/CopyEnvPreBuild.cs b/examples/demo/Assets/Scripts/Editor/CopyEnvPreBuild.cs new file mode 100644 index 000000000..17c5ae0d1 --- /dev/null +++ b/examples/demo/Assets/Scripts/Editor/CopyEnvPreBuild.cs @@ -0,0 +1,41 @@ +using System.IO; +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; + +namespace OneSignalDemo.Editor +{ + public class CopyEnvPreBuild : IPreprocessBuildWithReport + { + public int callbackOrder => 0; + + public void OnPreprocessBuild(BuildReport report) + { + var projectRoot = Path.GetDirectoryName(Application.dataPath); + var source = Path.Combine(projectRoot, ".env"); + var dest = Path.Combine(Application.streamingAssetsPath, ".env"); + + if (!File.Exists(source)) + { + Debug.LogWarning( + "[OneSignalDemo] No .env file found at project root. " + + "Live Activity API calls will be disabled. " + + "Copy .env.example to .env and add your key." + ); + if (File.Exists(dest)) + { + File.Delete(dest); + var metaPath = dest + ".meta"; + if (File.Exists(metaPath)) + File.Delete(metaPath); + } + return; + } + + Directory.CreateDirectory(Application.streamingAssetsPath); + File.Copy(source, dest, overwrite: true); + Debug.Log("[OneSignalDemo] Copied .env to StreamingAssets"); + } + } +} diff --git a/examples/demo/Assets/Scripts/Editor/CopyEnvPreBuild.cs.meta b/examples/demo/Assets/Scripts/Editor/CopyEnvPreBuild.cs.meta new file mode 100644 index 000000000..4fb392289 --- /dev/null +++ b/examples/demo/Assets/Scripts/Editor/CopyEnvPreBuild.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 22dfaece35ee843148765242503a6c02 \ No newline at end of file diff --git a/examples/demo/Assets/Scripts/Repositories/OneSignalRepository.cs b/examples/demo/Assets/Scripts/Repositories/OneSignalRepository.cs index 0c5b7968e..82b744336 100644 --- a/examples/demo/Assets/Scripts/Repositories/OneSignalRepository.cs +++ b/examples/demo/Assets/Scripts/Repositories/OneSignalRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using OneSignalDemo.Models; using OneSignalDemo.Services; using OneSignalSDK; @@ -112,5 +113,19 @@ public async Task SendCustomNotification(string title, string body) public async Task FetchUser(string onesignalId) => await _apiService.FetchUser(onesignalId); + + public bool HasApiKey() => _apiService.HasApiKey(); + + public void StartDefaultLiveActivity( + string activityId, + IDictionary attributes, + IDictionary content + ) => OneSignal.LiveActivities.StartDefault(activityId, attributes, content); + + public async Task UpdateLiveActivity( + string activityId, + string eventType, + JObject eventUpdates = null + ) => await _apiService.UpdateLiveActivity(activityId, eventType, eventUpdates); } } diff --git a/examples/demo/Assets/Scripts/Services/OneSignalApiService.cs b/examples/demo/Assets/Scripts/Services/OneSignalApiService.cs index 7c5f66b43..5ad84aadb 100644 --- a/examples/demo/Assets/Scripts/Services/OneSignalApiService.cs +++ b/examples/demo/Assets/Scripts/Services/OneSignalApiService.cs @@ -1,8 +1,10 @@ using System; +using System.IO; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using OneSignalDemo.Models; +using UnityEngine; using UnityEngine.Networking; namespace OneSignalDemo.Services @@ -10,14 +12,50 @@ namespace OneSignalDemo.Services public class OneSignalApiService { private string _appId; + private string _apiKey; private const string NotificationImageUrl = "https://media.onesignal.com/automated_push_templates/ratings_template.png"; + private const string PlaceholderApiKey = "your_rest_api_key"; + public void SetAppId(string appId) => _appId = appId; public string GetAppId() => _appId; + public void LoadApiKey() + { + var envPath = Path.Combine(Application.dataPath, "..", ".env"); +#if !UNITY_EDITOR + var streamingPath = Path.Combine(Application.streamingAssetsPath, ".env"); + if (File.Exists(streamingPath)) + envPath = streamingPath; +#endif + if (!File.Exists(envPath)) + return; + + foreach (var line in File.ReadAllLines(envPath)) + { + var trimmed = line.Trim(); + if (trimmed.StartsWith("#") || !trimmed.Contains("=")) + continue; + + var eqIndex = trimmed.IndexOf('='); + var key = trimmed.Substring(0, eqIndex).Trim(); + var value = trimmed.Substring(eqIndex + 1).Trim(); + int commentIdx = value.IndexOf('#'); + if (commentIdx >= 0) + value = value.Substring(0, commentIdx).Trim(); + value = value.Trim('"', '\''); + + if (key == "ONESIGNAL_API_KEY") + _apiKey = value; + } + } + + public bool HasApiKey() => + !string.IsNullOrEmpty(_apiKey) && _apiKey != PlaceholderApiKey; + public async Task SendNotification(NotificationType type, string subscriptionId) { if (string.IsNullOrEmpty(subscriptionId) || string.IsNullOrEmpty(_appId)) @@ -108,6 +146,53 @@ public async Task FetchUser(string onesignalId) return result; } + public async Task UpdateLiveActivity( + string activityId, + string eventType, + JObject eventUpdates = null + ) + { + if (string.IsNullOrEmpty(activityId) || string.IsNullOrEmpty(_appId) || !HasApiKey()) + return false; + + var encodedId = Uri.EscapeDataString(activityId); + var url = + $"https://api.onesignal.com/apps/{_appId}/live_activities/{encodedId}/notifications"; + + var payload = new JObject + { + ["event"] = eventType, + ["name"] = "Unity Demo Update", + ["priority"] = 10, + }; + + if (eventUpdates != null) + payload["event_updates"] = eventUpdates; + + if (eventType == "end") + { + var unixTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + payload["dismissal_date"] = unixTimestamp; + } + + var jsonPayload = payload.ToString(); + var request = new UnityWebRequest(url, "POST"); + var bodyRaw = Encoding.UTF8.GetBytes(jsonPayload); + request.uploadHandler = new UploadHandlerRaw(bodyRaw); + request.downloadHandler = new DownloadHandlerBuffer(); + request.SetRequestHeader("Content-Type", "application/json"); + request.SetRequestHeader("Authorization", $"Key {_apiKey}"); + + var tcs = new TaskCompletionSource(); + var operation = request.SendWebRequest(); + operation.completed += _ => tcs.TrySetResult(true); + await tcs.Task; + + bool success = request.responseCode >= 200 && request.responseCode < 300; + request.Dispose(); + return success; + } + private async Task PostNotification(string jsonPayload) { var request = new UnityWebRequest("https://onesignal.com/api/v1/notifications", "POST"); diff --git a/examples/demo/Assets/Scripts/UI/HomeScreenController.cs b/examples/demo/Assets/Scripts/UI/HomeScreenController.cs index 0ee0157d3..80b23bca3 100644 --- a/examples/demo/Assets/Scripts/UI/HomeScreenController.cs +++ b/examples/demo/Assets/Scripts/UI/HomeScreenController.cs @@ -41,6 +41,7 @@ public class HomeScreenController : MonoBehaviour private TriggersSectionController _triggersSection; private TrackEventSectionController _trackEventSection; private LocationSectionController _locationSection; + private LiveActivitiesSectionController _liveActivitiesSection; private void OnEnable() { @@ -254,11 +255,18 @@ private void BuildSections() _locationSection.OnInfoTap = () => ShowTooltip("location"); _contentRoot.Add(_locationSection.Root); +#if UNITY_IOS + _liveActivitiesSection = new LiveActivitiesSectionController(_viewModel); + _liveActivitiesSection.OnInfoTap = () => ShowTooltip("liveActivities"); + _contentRoot.Add(_liveActivitiesSection.Root); +#endif + var nextButton = SectionBuilder.CreatePrimaryButton( - "NEXT ACTIVITY", - "next_activity_button", + "NEXT SCREEN", + "next_screen_button", () => SceneManager.LoadScene("Secondary") ); + nextButton.style.marginTop = 24; _contentRoot.Add(nextButton); } @@ -280,6 +288,7 @@ private void RefreshAll() _tagsSection?.Refresh(); _triggersSection?.Refresh(); _locationSection?.Refresh(); + _liveActivitiesSection?.Refresh(); var showLoading = _viewModel.IsLoading; _loadingOverlay.style.display = showLoading ? DisplayStyle.Flex : DisplayStyle.None; diff --git a/examples/demo/Assets/Scripts/UI/SecondaryScreenController.cs b/examples/demo/Assets/Scripts/UI/SecondaryScreenController.cs index 4b69d3441..8a3b808d9 100644 --- a/examples/demo/Assets/Scripts/UI/SecondaryScreenController.cs +++ b/examples/demo/Assets/Scripts/UI/SecondaryScreenController.cs @@ -30,7 +30,7 @@ private void OnEnable() backButton.AddToClassList("back-button"); appBar.Add(backButton); - var title = new Label("Secondary Activity"); + var title = new Label("Secondary Screen"); title.AddToClassList("app-bar-title"); appBar.Add(title); @@ -39,7 +39,7 @@ private void OnEnable() var content = new VisualElement(); content.AddToClassList("centered-content"); - var heading = new Label("Secondary Activity"); + var heading = new Label("Secondary Screen"); heading.name = "secondary_heading"; heading.AddToClassList("page-heading"); content.Add(heading); diff --git a/examples/demo/Assets/Scripts/UI/Sections/LiveActivitiesSectionController.cs b/examples/demo/Assets/Scripts/UI/Sections/LiveActivitiesSectionController.cs new file mode 100644 index 000000000..00413d6dc --- /dev/null +++ b/examples/demo/Assets/Scripts/UI/Sections/LiveActivitiesSectionController.cs @@ -0,0 +1,145 @@ +using System; +using OneSignalDemo.ViewModels; +using UnityEngine.UIElements; + +namespace OneSignalDemo.UI.Sections +{ + public class LiveActivitiesSectionController + { + private readonly AppViewModel _viewModel; + private readonly VisualElement _root; + private TextField _activityIdField; + private TextField _orderNumberField; + private Button _startButton; + private Button _updateButton; + private Button _endButton; + + public Action OnInfoTap; + + public LiveActivitiesSectionController(AppViewModel viewModel) + { + _viewModel = viewModel; + _root = BuildSection(); + } + + public VisualElement Root => _root; + + private VisualElement BuildSection() + { + var section = SectionBuilder.CreateSection( + "Live Activities", + "live_activities_section", + () => OnInfoTap?.Invoke() + ); + + var inputCard = SectionBuilder.CreateCard("live_activities_input_card"); + + var activityIdRow = CreateInlineInputRow( + "Activity ID", + "order-1", + "live_activity_id_input" + ); + _activityIdField = activityIdRow.Q(); + _activityIdField.RegisterValueChangedCallback(_ => RefreshButtonStates()); + inputCard.Add(activityIdRow); + + inputCard.Add(SectionBuilder.CreateDivider(true)); + + var orderNumberRow = CreateInlineInputRow( + "Order #", + "ORD-1234", + "live_activity_order_input" + ); + _orderNumberField = orderNumberRow.Q(); + inputCard.Add(orderNumberRow); + + section.Add(inputCard); + + _startButton = SectionBuilder.CreatePrimaryButton( + "START LIVE ACTIVITY", + "start_live_activity_button", + OnStartTap + ); + section.Add(_startButton); + + _updateButton = SectionBuilder.CreatePrimaryButton( + $"UPDATE \u2192 {_viewModel.NextStatusLabel}", + "update_live_activity_button", + OnUpdateTap + ); + section.Add(_updateButton); + + _endButton = SectionBuilder.CreateDestructiveButton( + "END LIVE ACTIVITY", + "end_live_activity_button", + OnEndTap + ); + section.Add(_endButton); + + RefreshButtonStates(); + return section; + } + + public void Refresh() + { + RefreshButtonStates(); + } + + private void RefreshButtonStates() + { + bool hasActivityId = !string.IsNullOrEmpty(_activityIdField?.value); + bool hasApiKey = _viewModel.HasApiKey; + + _startButton?.SetEnabled(hasActivityId && !_viewModel.IsLiveActivityUpdating); + + bool canUpdate = hasActivityId && hasApiKey && !_viewModel.IsLiveActivityUpdating; + _updateButton?.SetEnabled(canUpdate); + if (_updateButton != null) + _updateButton.text = $"UPDATE \u2192 {_viewModel.NextStatusLabel}"; + + _endButton?.SetEnabled(hasActivityId && hasApiKey && !_viewModel.IsLiveActivityUpdating); + } + + private void OnStartTap() + { + var activityId = _activityIdField?.value; + var orderNumber = string.IsNullOrEmpty(_orderNumberField?.value) ? "ORD-1234" : _orderNumberField.value; + _viewModel.StartLiveActivity(activityId, orderNumber); + } + + private void OnUpdateTap() + { + var activityId = _activityIdField?.value; + _viewModel.UpdateLiveActivity(activityId); + } + + private void OnEndTap() + { + var activityId = _activityIdField?.value; + _viewModel.EndLiveActivity(activityId); + } + + private static VisualElement CreateInlineInputRow( + string label, + string defaultValue, + string name + ) + { + var row = new VisualElement(); + row.AddToClassList("toggle-row"); + + var labelElement = new Label(label); + labelElement.AddToClassList("toggle-label"); + labelElement.AddToClassList("text-toggle-label"); + row.Add(labelElement); + + var field = new TextField(); + field.name = name; + field.value = defaultValue; + field.AddToClassList("inline-input-field"); + row.Add(field); + + return row; + } + } +} diff --git a/examples/demo/Assets/Scripts/UI/Sections/LiveActivitiesSectionController.cs.meta b/examples/demo/Assets/Scripts/UI/Sections/LiveActivitiesSectionController.cs.meta new file mode 100644 index 000000000..57cd4d245 --- /dev/null +++ b/examples/demo/Assets/Scripts/UI/Sections/LiveActivitiesSectionController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ac01c2e8884d34f17bb76140778c7b7b \ 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 4f85cbe0f..63fa18b89 100644 --- a/examples/demo/Assets/Scripts/ViewModels/AppViewModel.cs +++ b/examples/demo/Assets/Scripts/ViewModels/AppViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using OneSignalDemo.Models; using OneSignalDemo.Repositories; using OneSignalDemo.Services; @@ -36,6 +37,19 @@ public class AppViewModel : MonoBehaviour private List> _tagsList = new(); private List> _triggersList = new(); + private int _liveActivityStatusIndex; + private bool _isLiveActivityUpdating; + + private static readonly string[] LiveActivityStatuses = { "preparing", "on_the_way", "delivered" }; + private static readonly string[] LiveActivityMessages = + { + "Your order is being prepared", + "Driver is heading your way", + "Order delivered!", + }; + private static readonly string[] LiveActivityETAs = { "15 min", "10 min", "" }; + private static readonly string[] LiveActivityStatusLabels = { "PREPARING", "ON THE WAY", "DELIVERED" }; + public string AppId => _appId; public bool ConsentRequired => _consentRequired; public bool PrivacyConsentGiven => _privacyConsentGiven; @@ -54,6 +68,19 @@ public class AppViewModel : MonoBehaviour public IReadOnlyList> Tags => _tagsList; public IReadOnlyList> Triggers => _triggersList; + public int LiveActivityStatusIndex => _liveActivityStatusIndex; + public bool IsLiveActivityUpdating => _isLiveActivityUpdating; + public bool HasApiKey => _repository?.HasApiKey() ?? false; + + public string NextStatusLabel + { + get + { + int nextIndex = (_liveActivityStatusIndex + 1) % LiveActivityStatuses.Length; + return LiveActivityStatusLabels[nextIndex]; + } + } + public event Action OnStateChanged; public event Action OnToastMessage; @@ -368,6 +395,114 @@ public async void SendCustomNotification(string title, string body) } } + public void StartLiveActivity(string activityId, string orderNumber) + { + if (string.IsNullOrEmpty(activityId)) + return; + + var attributes = new Dictionary { { "orderNumber", orderNumber } }; + var content = new Dictionary + { + { "status", LiveActivityStatuses[0] }, + { "message", LiveActivityMessages[0] }, + { "estimatedTime", LiveActivityETAs[0] }, + }; + + _repository.StartDefaultLiveActivity(activityId, attributes, content); + _liveActivityStatusIndex = 0; + + LogManager.Instance.Info(Tag, $"Started Live Activity: {activityId}"); + ShowToast($"Started Live Activity: {activityId}"); + NotifyStateChanged(); + } + + public async void UpdateLiveActivity(string activityId) + { + if (string.IsNullOrEmpty(activityId) || _isLiveActivityUpdating) + return; + + _isLiveActivityUpdating = true; + NotifyStateChanged(); + + try + { + int nextIndex = (_liveActivityStatusIndex + 1) % LiveActivityStatuses.Length; + var eventUpdates = new JObject + { + ["data"] = new JObject + { + ["status"] = LiveActivityStatuses[nextIndex], + ["message"] = LiveActivityMessages[nextIndex], + ["estimatedTime"] = LiveActivityETAs[nextIndex], + }, + }; + + bool success = await _repository.UpdateLiveActivity(activityId, "update", eventUpdates); + if (success) + { + _liveActivityStatusIndex = nextIndex; + LogManager.Instance.Info(Tag, $"Updated Live Activity: {activityId}"); + ShowToast($"Updated Live Activity: {activityId}"); + } + else + { + LogManager.Instance.Error(Tag, "Failed to update Live Activity"); + ShowToast("Failed to update Live Activity"); + } + } + catch (Exception ex) + { + LogManager.Instance.Error(Tag, $"Live Activity update error: {ex.Message}"); + ShowToast("Failed to update Live Activity"); + } + + _isLiveActivityUpdating = false; + NotifyStateChanged(); + } + + public async void EndLiveActivity(string activityId) + { + if (string.IsNullOrEmpty(activityId) || _isLiveActivityUpdating) + return; + + _isLiveActivityUpdating = true; + NotifyStateChanged(); + + try + { + var eventUpdates = new JObject + { + ["data"] = new JObject + { + ["status"] = "delivered", + ["message"] = "Ended", + ["estimatedTime"] = "", + }, + }; + + bool success = await _repository.UpdateLiveActivity(activityId, "end", eventUpdates); + if (success) + { + _liveActivityStatusIndex = 0; + LogManager.Instance.Info(Tag, $"Ended Live Activity: {activityId}"); + ShowToast($"Ended Live Activity: {activityId}"); + } + else + { + LogManager.Instance.Error(Tag, "Failed to end Live Activity"); + ShowToast("Failed to end Live Activity"); + } + } + catch (Exception ex) + { + LogManager.Instance.Error(Tag, $"Live Activity end error: {ex.Message}"); + ShowToast("Failed to end Live Activity"); + } + + _isLiveActivityUpdating = false; + NotifyStateChanged(); + } + public void SetConsentRequired(bool required) { _consentRequired = required; diff --git a/examples/demo/iOS/ExampleWidget/ExampleWidgetBundle.swift b/examples/demo/iOS/ExampleWidget/ExampleWidgetBundle.swift deleted file mode 100644 index 5454c36b6..000000000 --- a/examples/demo/iOS/ExampleWidget/ExampleWidgetBundle.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ExampleWidgetBundle.swift -// ExampleWidget -// -// Created by Brian Smith on 5/29/24. -// - -#if !targetEnvironment(macCatalyst) -import WidgetKit -import SwiftUI - -@main -struct ExampleWidgetBundle: WidgetBundle { - var body: some Widget { - ExampleWidgetLiveActivity() - } -} -#endif diff --git a/examples/demo/iOS/ExampleWidget/ExampleWidgetLiveActivity.swift b/examples/demo/iOS/ExampleWidget/ExampleWidgetLiveActivity.swift deleted file mode 100644 index 6d627f527..000000000 --- a/examples/demo/iOS/ExampleWidget/ExampleWidgetLiveActivity.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// ExampleWidgetLiveActivity.swift -// ExampleWidget -// -// Created by Brian Smith on 4/30/24. -// Copyright © 2024 The Chromium Authors. All rights reserved. -// - -#if !targetEnvironment(macCatalyst) -import ActivityKit -import WidgetKit -import SwiftUI -import OneSignalLiveActivities - -struct ExampleWidgetLiveActivity: Widget { - var body: some WidgetConfiguration { - ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in - // Lock screen/banner UI goes here\VStack(alignment: .leading) { - VStack { - Spacer() - Text("UNITY: " + (context.attributes.data["title"]?.asString() ?? "")).font(.headline) - Spacer() - HStack { - Spacer() - Label { - Text(context.state.data["message"]?.asDict()?["en"]?.asString() ?? "") - } icon: { - Image("onesignaldemo") - .resizable() - .scaledToFit() - .frame(width: 40.0, height: 40.0) - } - Spacer() - } - Text("INT: " + String(context.state.data["intValue"]?.asInt() ?? 0)) - Text("DBL: " + String(context.state.data["doubleValue"]?.asDouble() ?? 0.0)) - Text("BOL: " + String(context.state.data["boolValue"]?.asBool() ?? false)) - Spacer() - } - .activitySystemActionForegroundColor(.black) - .activityBackgroundTint(.white) - } dynamicIsland: { _ in - DynamicIsland { - // Expanded UI goes here. Compose the expanded UI through - // various regions, like leading/trailing/center/bottom - DynamicIslandExpandedRegion(.leading) { - Text("Leading") - } - DynamicIslandExpandedRegion(.trailing) { - Text("Trailing") - } - DynamicIslandExpandedRegion(.bottom) { - Text("Bottom") - // more content - } - } compactLeading: { - Text("L") - } compactTrailing: { - Text("T") - } minimal: { - Text("Min") - } - .widgetURL(URL(string: "http://www.apple.com")) - .keylineTint(Color.red) - } - } -} -#endif diff --git a/examples/demo/iOS/ExampleWidget.meta b/examples/demo/iOS/OneSignalWidget.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget.meta rename to examples/demo/iOS/OneSignalWidget.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/AccentColor.colorset.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/AccentColor.colorset.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/AccentColor.colorset.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/AccentColor.colorset.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/AccentColor.colorset/Contents.json rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/AccentColor.colorset/Contents.json.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/AccentColor.colorset/Contents.json.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/AppIcon.appiconset.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/AppIcon.appiconset.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/AppIcon.appiconset.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/AppIcon.appiconset.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/AppIcon.appiconset/Contents.json rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/AppIcon.appiconset/Contents.json.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/AppIcon.appiconset/Contents.json.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/Contents.json b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/Contents.json similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/Contents.json rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/Contents.json diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/Contents.json.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/Contents.json.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/Contents.json.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/Contents.json.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/WidgetBackground.colorset.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/WidgetBackground.colorset.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/Contents.json.meta diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png diff --git a/examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png.meta b/examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png.meta rename to examples/demo/iOS/OneSignalWidget/Assets.xcassets/onesignaldemo.imageset/onesignal-logo.png.meta diff --git a/examples/demo/iOS/ExampleWidget/Info.plist b/examples/demo/iOS/OneSignalWidget/Info.plist similarity index 96% rename from examples/demo/iOS/ExampleWidget/Info.plist rename to examples/demo/iOS/OneSignalWidget/Info.plist index f42024eb2..941dc7476 100644 --- a/examples/demo/iOS/ExampleWidget/Info.plist +++ b/examples/demo/iOS/OneSignalWidget/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - ExampleWidget + OneSignalWidget CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/examples/demo/iOS/ExampleWidget/Info.plist.meta b/examples/demo/iOS/OneSignalWidget/Info.plist.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/Info.plist.meta rename to examples/demo/iOS/OneSignalWidget/Info.plist.meta diff --git a/examples/demo/iOS/OneSignalWidget/OneSignalWidgetBundle.swift b/examples/demo/iOS/OneSignalWidget/OneSignalWidgetBundle.swift new file mode 100644 index 000000000..04962e771 --- /dev/null +++ b/examples/demo/iOS/OneSignalWidget/OneSignalWidgetBundle.swift @@ -0,0 +1,11 @@ +#if !targetEnvironment(macCatalyst) +import WidgetKit +import SwiftUI + +@main +struct OneSignalWidgetBundle: WidgetBundle { + var body: some Widget { + OneSignalWidgetLiveActivity() + } +} +#endif diff --git a/examples/demo/iOS/ExampleWidget/ExampleWidgetBundle.swift.meta b/examples/demo/iOS/OneSignalWidget/OneSignalWidgetBundle.swift.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/ExampleWidgetBundle.swift.meta rename to examples/demo/iOS/OneSignalWidget/OneSignalWidgetBundle.swift.meta diff --git a/examples/demo/iOS/OneSignalWidget/OneSignalWidgetLiveActivity.swift b/examples/demo/iOS/OneSignalWidget/OneSignalWidgetLiveActivity.swift new file mode 100644 index 000000000..6421737b3 --- /dev/null +++ b/examples/demo/iOS/OneSignalWidget/OneSignalWidgetLiveActivity.swift @@ -0,0 +1,144 @@ +#if !targetEnvironment(macCatalyst) +import ActivityKit +import WidgetKit +import SwiftUI +import OneSignalLiveActivities + +@available(iOS 16.2, *) +struct OneSignalWidgetLiveActivity: Widget { + + private func statusIcon(for status: String) -> String { + switch status { + case "on_the_way": return "box.truck.fill" + case "delivered": return "checkmark.circle.fill" + default: return "bag.fill" + } + } + + private func statusColor(for status: String) -> Color { + switch status { + case "on_the_way": return .blue + case "delivered": return .green + default: return .orange + } + } + + private func statusLabel(for status: String) -> String { + switch status { + case "on_the_way": return "On the Way" + case "delivered": return "Delivered" + default: return "Preparing" + } + } + + var body: some WidgetConfiguration { + ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in + let orderNumber = context.attributes.data["orderNumber"]?.asString() ?? "Order" + let status = context.state.data["status"]?.asString() ?? "preparing" + let message = context.state.data["message"]?.asString() ?? "Your order is being prepared" + let eta = context.state.data["estimatedTime"]?.asString() ?? "" + + VStack(spacing: 10) { + HStack { + Text(orderNumber) + .font(.caption) + .foregroundColor(.gray) + Spacer() + if !eta.isEmpty { + Text(eta) + .font(.caption) + .foregroundColor(.white.opacity(0.7)) + } + } + + HStack(spacing: 12) { + Image(systemName: statusIcon(for: status)) + .font(.title2) + .foregroundColor(statusColor(for: status)) + + VStack(alignment: .leading, spacing: 2) { + Text(statusLabel(for: status)) + .font(.headline) + .foregroundColor(.white) + Text(message) + .font(.subheadline) + .foregroundColor(.white.opacity(0.8)) + .lineLimit(1) + } + Spacer() + } + + DeliveryProgressBar(status: status) + } + .padding() + .activityBackgroundTint(Color(red: 0.11, green: 0.13, blue: 0.19)) + .activitySystemActionForegroundColor(.white) + + } dynamicIsland: { context in + let status = context.state.data["status"]?.asString() ?? "preparing" + let message = context.state.data["message"]?.asString() ?? "Preparing" + let eta = context.state.data["estimatedTime"]?.asString() ?? "" + + return DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + Image(systemName: statusIcon(for: status)) + .font(.title2) + .foregroundColor(statusColor(for: status)) + } + DynamicIslandExpandedRegion(.center) { + Text(statusLabel(for: status)) + .font(.headline) + } + DynamicIslandExpandedRegion(.trailing) { + if !eta.isEmpty { + Text(eta) + .font(.caption) + .foregroundColor(.secondary) + } + } + DynamicIslandExpandedRegion(.bottom) { + Text(message) + .font(.caption) + .foregroundColor(.secondary) + } + } compactLeading: { + Image(systemName: statusIcon(for: status)) + .foregroundColor(statusColor(for: status)) + } compactTrailing: { + Text(statusLabel(for: status)) + .font(.caption) + } minimal: { + Image(systemName: statusIcon(for: status)) + .foregroundColor(statusColor(for: status)) + } + } + } +} + +@available(iOS 16.2, *) +struct DeliveryProgressBar: View { + let status: String + + private var progress: CGFloat { + switch status { + case "on_the_way": return 0.6 + case "delivered": return 1.0 + default: return 0.25 + } + } + + var body: some View { + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)) + .frame(height: 6) + RoundedRectangle(cornerRadius: 3) + .fill(progress >= 1.0 ? Color.green : Color.blue) + .frame(width: geo.size.width * progress, height: 6) + } + } + .frame(height: 6) + } +} +#endif diff --git a/examples/demo/iOS/ExampleWidget/ExampleWidgetLiveActivity.swift.meta b/examples/demo/iOS/OneSignalWidget/OneSignalWidgetLiveActivity.swift.meta similarity index 100% rename from examples/demo/iOS/ExampleWidget/ExampleWidgetLiveActivity.swift.meta rename to examples/demo/iOS/OneSignalWidget/OneSignalWidgetLiveActivity.swift.meta