-
Notifications
You must be signed in to change notification settings - Fork 6
feat: [SDK-4788] add iOS manual notification forwarding #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| using Foundation; | ||
| using UIKit; | ||
| using UserNotifications; | ||
| using OneSignalNative = Com.OneSignal.iOS.OneSignal; | ||
|
|
||
| namespace OneSignalSDK.DotNet.iOS; | ||
|
|
||
| /// <summary> | ||
| /// Supported iOS notification callbacks for apps that disable OneSignal method swizzling. | ||
| /// </summary> | ||
| public static class NotificationsManualIntegration | ||
| { | ||
| private const string DisableSwizzlingKey = "OneSignal_disable_swizzling"; | ||
|
|
||
| /// <summary> | ||
| /// Returns whether manual notification forwarding is enabled in the app's Info.plist. | ||
| /// </summary> | ||
| public static bool IsEnabled => | ||
| (NSBundle.MainBundle.ObjectForInfoDictionary(DisableSwizzlingKey) as NSNumber)?.BoolValue | ||
| ?? false; | ||
|
|
||
| /// <summary> | ||
| /// Returns whether a notification payload belongs to OneSignal. | ||
| /// </summary> | ||
| public static bool IsOneSignalNotification(NSDictionary payload) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(payload); | ||
| return Com.OneSignal.iOS.OneSignalCoreHelper.IsOneSignalPayload(payload); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forwards successful APNs registration to OneSignal. | ||
| /// </summary> | ||
| public static void DidRegisterForRemoteNotifications(NSData deviceToken) | ||
| { | ||
| EnsureEnabled(); | ||
| ArgumentNullException.ThrowIfNull(deviceToken); | ||
| OneSignalNative.Notifications.DidRegisterForRemoteNotifications(deviceToken); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forwards failed APNs registration to OneSignal. | ||
| /// </summary> | ||
| public static void DidFailToRegisterForRemoteNotifications(NSError error) | ||
| { | ||
| EnsureEnabled(); | ||
| ArgumentNullException.ThrowIfNull(error); | ||
| OneSignalNative.Notifications.DidFailToRegisterForRemoteNotifications(error); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Forwards a background remote notification to OneSignal. OneSignal owns the completion handler. | ||
| /// </summary> | ||
| public static void DidReceiveRemoteNotification( | ||
| NSDictionary userInfo, | ||
| Action<UIBackgroundFetchResult> completionHandler | ||
| ) | ||
| { | ||
| EnsureEnabled(); | ||
| ArgumentNullException.ThrowIfNull(userInfo); | ||
| ArgumentNullException.ThrowIfNull(completionHandler); | ||
| OneSignalNative.Notifications.DidReceiveRemoteNotification(userInfo, completionHandler); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles a OneSignal foreground notification and completes Apple's callback exactly once. | ||
| /// </summary> | ||
| /// <returns><see langword="true"/> when the notification belonged to OneSignal.</returns> | ||
| public static bool TryHandleWillPresentNotification( | ||
| UNNotification notification, | ||
| UNNotificationPresentationOptions presentationOptions, | ||
| Action<UNNotificationPresentationOptions> completionHandler | ||
| ) | ||
| { | ||
| EnsureEnabled(); | ||
| ArgumentNullException.ThrowIfNull(notification); | ||
| ArgumentNullException.ThrowIfNull(completionHandler); | ||
|
|
||
| var payload = notification.Request.Content.UserInfo; | ||
| if (!IsOneSignalNotification(payload)) | ||
| return false; | ||
|
|
||
| OneSignalNative.Notifications.WillPresentNotification( | ||
| payload, | ||
| displayableNotification => | ||
| completionHandler( | ||
| displayableNotification == null | ||
| ? (UNNotificationPresentationOptions)0 | ||
| : presentationOptions | ||
| ) | ||
| ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small asymmetry with Native |
||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Handles a OneSignal notification response and completes Apple's callback exactly once. | ||
| /// </summary> | ||
| /// <returns><see langword="true"/> when the notification belonged to OneSignal.</returns> | ||
| public static bool TryHandleNotificationResponse( | ||
| UNNotificationResponse response, | ||
| Action completionHandler | ||
| ) | ||
| { | ||
| EnsureEnabled(); | ||
| ArgumentNullException.ThrowIfNull(response); | ||
| ArgumentNullException.ThrowIfNull(completionHandler); | ||
|
|
||
| if (!IsOneSignalNotification(response.Notification.Request.Content.UserInfo)) | ||
| return false; | ||
|
|
||
| try | ||
| { | ||
| OneSignalNative.Notifications.DidReceiveNotificationResponse(response); | ||
| } | ||
| finally | ||
| { | ||
| completionHandler(); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sets the app icon badge count through OneSignal. | ||
| /// </summary> | ||
| public static void SetBadgeCount(nint badgeCount) | ||
| { | ||
| EnsureEnabled(); | ||
| OneSignalNative.Notifications.SetBadgeCount(badgeCount); | ||
| } | ||
|
|
||
| private static void EnsureEnabled() | ||
| { | ||
| if (!IsEnabled) | ||
| { | ||
| throw new InvalidOperationException( | ||
| $"Set {DisableSwizzlingKey} to true in Info.plist before using manual integration." | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1 @@ | ||
| # Default App ID (used when ONESIGNAL_APP_ID is empty or missing): 77e32082-ea27-42e3-a898-c72e141824ef | ||
| ONESIGNAL_APP_ID=your-onesignal-app-id |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| using System.Text; | ||
| using System.Text.Json; | ||
| using OneSignalSDK.DotNet; | ||
| using OneSignalSDK.DotNet.Core.Notifications; | ||
| using OneSignalSDK.DotNet.Core.User; | ||
|
|
@@ -88,6 +90,24 @@ public MainPage() | |
| ); | ||
| }; | ||
|
|
||
| var showOneSignalNotificationButton = new Button | ||
| { | ||
| Text = "Show OneSignal Notification", | ||
| AutomationId = "show_onesignal_notification_button", | ||
| }; | ||
| showOneSignalNotificationButton.Clicked += async (s, e) => | ||
| { | ||
| showOneSignalNotificationButton.IsEnabled = false; | ||
| try | ||
| { | ||
| await SendSimpleOneSignalNotificationAsync(); | ||
| } | ||
| finally | ||
| { | ||
| showOneSignalNotificationButton.IsEnabled = true; | ||
| } | ||
| }; | ||
|
|
||
| var clearButton = new Button | ||
| { | ||
| Text = "Clear Delivered Notifications", | ||
|
|
@@ -133,6 +153,7 @@ public MainPage() | |
| requestOneSignalPermissionButton, | ||
| requestLocalPermissionButton, | ||
| showLocalNotificationButton, | ||
| showOneSignalNotificationButton, | ||
| clearButton, | ||
| refreshPushInfoButton, | ||
| _statusLabel, | ||
|
|
@@ -160,10 +181,9 @@ private void RefreshPushInfoLabel() | |
| { | ||
| var pushSubscription = OneSignal.User.PushSubscription; | ||
| _pushInfoLabel.Text = | ||
| $"OneSignal ID: {FormatValue(OneSignal.User.OneSignalId)}\n" | ||
| + $"Push subscription ID: {FormatValue(pushSubscription.Id)}\n" | ||
| + $"Push opted in: {pushSubscription.OptedIn}\n" | ||
| + $"Push token: {FormatValue(pushSubscription.Token)}"; | ||
| $"OneSignal ID:\n{FormatValue(OneSignal.User.OneSignalId)}\n" | ||
| + $"Push subscription ID:\n{FormatValue(pushSubscription.Id)}\n" | ||
| + $"Push opted in: {pushSubscription.OptedIn}"; | ||
| } | ||
|
|
||
| private void OnPermissionChanged(object? sender, NotificationPermissionChangedEventArgs args) | ||
|
|
@@ -185,6 +205,51 @@ private void OnUserChanged(object? sender, UserStateChangedEventArgs args) | |
| MainThread.BeginInvokeOnMainThread(RefreshPushInfoLabel); | ||
| } | ||
|
|
||
| private async Task SendSimpleOneSignalNotificationAsync() | ||
| { | ||
| if (!DotEnv.HasOneSignalAppId) | ||
| { | ||
| SetStatus("Set ONESIGNAL_APP_ID in .env before sending a OneSignal notification."); | ||
| return; | ||
| } | ||
|
|
||
| var pushSubscriptionId = OneSignal.User.PushSubscription.Id; | ||
| if (string.IsNullOrWhiteSpace(pushSubscriptionId)) | ||
| { | ||
| SetStatus( | ||
| "No OneSignal push subscription ID yet. Refresh after permission is granted." | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| using var client = new HttpClient(); | ||
| client.DefaultRequestHeaders.Add("Accept", "application/vnd.onesignal.v1+json"); | ||
|
|
||
| var payload = new Dictionary<string, object> | ||
| { | ||
| ["app_id"] = DotEnv.OneSignalAppId, | ||
| ["headings"] = new Dictionary<string, string> { ["en"] = "Simple Notification" }, | ||
| ["contents"] = new Dictionary<string, string> | ||
| { | ||
| ["en"] = "This is a simple push notification", | ||
| }, | ||
| ["include_subscription_ids"] = new[] { pushSubscriptionId }, | ||
| }; | ||
|
|
||
| var json = JsonSerializer.Serialize(payload); | ||
| var response = await client.PostAsync( | ||
| "https://onesignal.com/api/v1/notifications", | ||
| new StringContent(json, Encoding.UTF8, "application/json") | ||
| ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This POST has no Two related concerns:
I’d either drop the button and document “send a test push from the dashboard to the subscription ID shown above,” or route send through something that never embeds a REST key in the client. Matching the main demo’s auth-less pattern doesn’t really help here because this sample is specifically selling that button as the verification path. |
||
| var responseJson = await response.Content.ReadAsStringAsync(); | ||
|
|
||
| SetStatus( | ||
| response.IsSuccessStatusCode | ||
| ? $"OneSignal notification requested: {responseJson}" | ||
| : $"OneSignal notification failed: {responseJson}" | ||
| ); | ||
| } | ||
|
|
||
| private void SetStatus(string message) | ||
| { | ||
| MainThread.BeginInvokeOnMainThread(() => | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both
TryHandle*methods callEnsureEnabled()before theIsOneSignalNotificationgate.EnsureEnabledthrows when the plist flag is missing, so we never get toreturn falseand the coordinator never falls through to the plugin delegate.In the example that means a missing
OneSignal_disable_swizzlingkey doesn’t quietly skip OneSignal — it aborts the wholeWillPresent/DidReceiveResponsepath, Apple’s completion never runs, and local notifications hang too. That’s a much worse failure mode than “OneSignal just doesn’t get the callback.”I’d either:
EnsureEnabledonce we know it’s ours, orreturn false(log + fall through) instead of throwing on these two entry points.The throw still makes sense on the unconditional AppDelegate forwards (
DidRegister…,DidReceiveRemoteNotification, etc.), where there’s no alternate owner to hand off to.