Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/plugin-local-notif/.env.example
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
7 changes: 7 additions & 0 deletions examples/plugin-local-notif/DotEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace PluginLocalNotifDemo;

public static class DotEnv
{
public const string OneSignalAppIdPlaceholder = "your-onesignal-app-id";

private static readonly Dictionary<string, string> Values = new();
private static bool _loaded;

Expand Down Expand Up @@ -54,4 +56,9 @@ var line in reader.ReadToEnd().Split('\n', StringSplitOptions.RemoveEmptyEntries
}

public static string Get(string key) => Values.TryGetValue(key, out var value) ? value : "";

public static string OneSignalAppId => Get("ONESIGNAL_APP_ID").Trim();

public static bool HasOneSignalAppId =>
!string.IsNullOrWhiteSpace(OneSignalAppId) && OneSignalAppId != OneSignalAppIdPlaceholder;
}
110 changes: 103 additions & 7 deletions examples/plugin-local-notif/MainPage.cs
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;
Expand Down Expand Up @@ -33,9 +35,12 @@ public MainPage()
LineBreakMode = LineBreakMode.WordWrap,
};

OneSignal.Notifications.PermissionChanged += OnPermissionChanged;
OneSignal.User.PushSubscription.Changed += OnPushSubscriptionChanged;
OneSignal.User.Changed += OnUserChanged;
if (DotEnv.HasOneSignalAppId)
{
OneSignal.Notifications.PermissionChanged += OnPermissionChanged;
OneSignal.User.PushSubscription.Changed += OnPushSubscriptionChanged;
OneSignal.User.Changed += OnUserChanged;
}

var requestOneSignalPermissionButton = new Button
{
Expand All @@ -44,6 +49,12 @@ public MainPage()
};
requestOneSignalPermissionButton.Clicked += async (s, e) =>
{
if (!DotEnv.HasOneSignalAppId)
{
SetStatus("Set ONESIGNAL_APP_ID in .env before requesting OneSignal permission.");
return;
}

var granted = await OneSignal.Notifications.RequestPermissionAsync(true);
SetStatus($"OneSignal permission granted: {granted}");
RefreshPermissionLabel();
Expand Down Expand Up @@ -88,13 +99,41 @@ 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();
}
catch (Exception exception)
{
SetStatus($"OneSignal notification failed: {exception.Message}");
}
finally
{
showOneSignalNotificationButton.IsEnabled = true;
}
};

var clearButton = new Button
{
Text = "Clear Delivered Notifications",
AutomationId = "clear_notifications_button",
};
clearButton.Clicked += (s, e) =>
{
if (!DotEnv.HasOneSignalAppId)
{
SetStatus("Set ONESIGNAL_APP_ID in .env before clearing OneSignal notifications.");
return;
}

OneSignal.Notifications.ClearAllNotifications();
SetStatus("Cleared delivered notifications through OneSignal.");
};
Expand Down Expand Up @@ -133,6 +172,7 @@ public MainPage()
requestOneSignalPermissionButton,
requestLocalPermissionButton,
showLocalNotificationButton,
showOneSignalNotificationButton,
clearButton,
refreshPushInfoButton,
_statusLabel,
Expand All @@ -153,17 +193,28 @@ protected override void OnAppearing()

private void RefreshPermissionLabel()
{
if (!DotEnv.HasOneSignalAppId)
{
_permissionLabel.Text = "OneSignal permission: unavailable (app ID not set)";
return;
}

_permissionLabel.Text = $"OneSignal permission: {OneSignal.Notifications.Permission}";
}

private void RefreshPushInfoLabel()
{
if (!DotEnv.HasOneSignalAppId)
{
_pushInfoLabel.Text = "OneSignal push subscription: unavailable (app ID not set)";
return;
}

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)
Expand All @@ -185,6 +236,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")
);
var responseJson = await response.Content.ReadAsStringAsync();

SetStatus(
response.IsSuccessStatusCode
? $"OneSignal notification requested: {responseJson}"
: $"OneSignal notification failed: {responseJson}"
);
}
Comment thread
fadi-george marked this conversation as resolved.

private void SetStatus(string message)
{
MainThread.BeginInvokeOnMainThread(() =>
Expand Down
33 changes: 23 additions & 10 deletions examples/plugin-local-notif/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,39 @@ namespace PluginLocalNotifDemo;

public static class MauiProgram
{
private const string DefaultAppId = "77e32082-ea27-42e3-a898-c72e141824ef";

public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();

builder.UseMauiApp<App>().UseLocalNotification();
builder
.UseMauiApp<App>()
.UseLocalNotification(options =>
{
#if IOS
options.AddiOS(ios =>
{
ios.SetCustomUserNotificationCenterDelegate(
new OneSignalCompatibleNotificationDelegate()
);
});
#endif
});

var app = builder.Build();

DotEnv.Load();

var envAppId = DotEnv.Get("ONESIGNAL_APP_ID");
var appId =
string.IsNullOrWhiteSpace(envAppId) || envAppId == "your-onesignal-app-id"
? DefaultAppId
: envAppId.Trim();

OneSignal.Debug.LogLevel = OsLogLevel.VERBOSE;
OneSignal.Initialize(appId);
if (DotEnv.HasOneSignalAppId)
{
OneSignal.Initialize(DotEnv.OneSignalAppId);
}
else
{
System.Diagnostics.Debug.WriteLine(
"Set ONESIGNAL_APP_ID in .env to initialize OneSignal."
);
}

OneSignal.Notifications.WillDisplay += (s, e) =>
System.Diagnostics.Debug.WriteLine("OneSignal notification will display");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Foundation;
using Plugin.LocalNotification.Platforms;
using UserNotifications;

namespace PluginLocalNotifDemo;

public sealed class OneSignalCompatibleNotificationDelegate : UserNotificationCenterDelegate
{
// OneSignal's swizzle can exchange method implementations on this class, so
// depending on which slot iOS invokes, either the normal overrides or the
// onesignal-prefixed exports receive the callback. Both route remote pushes
// to foreground presentation and leave local notifications to the plugin.
public override void WillPresentNotification(
UNUserNotificationCenter center,
UNNotification notification,
Action<UNNotificationPresentationOptions> completionHandler
)
{
if (HandledAsRemotePush(notification, completionHandler))
return;

base.WillPresentNotification(center, notification, completionHandler);
}

public override void DidReceiveNotificationResponse(
UNUserNotificationCenter center,
UNNotificationResponse response,
Action completionHandler
)
{
if (response.Notification.Request.Trigger is UNPushNotificationTrigger)
{
completionHandler();
return;
}

base.DidReceiveNotificationResponse(center, response, completionHandler);
}

[Export("onesignalUserNotificationCenter:willPresentNotification:withCompletionHandler:")]
public void OneSignalWillPresentNotification(
UNUserNotificationCenter center,
UNNotification notification,
Action<UNNotificationPresentationOptions> completionHandler
)
{
if (HandledAsRemotePush(notification, completionHandler))
return;

base.WillPresentNotification(center, notification, completionHandler);
}

[Export(
"onesignalUserNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:"
)]
public void OneSignalDidReceiveNotificationResponse(
UNUserNotificationCenter center,
UNNotificationResponse response,
Action completionHandler
)
{
if (response.Notification.Request.Trigger is UNPushNotificationTrigger)
{
completionHandler();
return;
}

base.DidReceiveNotificationResponse(center, response, completionHandler);
}

private static bool HandledAsRemotePush(
UNNotification notification,
Action<UNNotificationPresentationOptions> completionHandler
)
{
if (notification.Request.Trigger is not UNPushNotificationTrigger)
return false;

completionHandler(
UNNotificationPresentationOptions.Banner
| UNNotificationPresentationOptions.List
| UNNotificationPresentationOptions.Sound
| UNNotificationPresentationOptions.Badge
);
return true;
}
}
36 changes: 29 additions & 7 deletions examples/plugin-local-notif/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Minimal .NET MAUI app for investigating the iOS interaction reported in
[GitHub issue #132](https://github.com/OneSignal/OneSignal-DotNet-SDK/issues/132).

This example is a temporary compatibility workaround, not a first-party
OneSignal local notification implementation. OneSignal hopes to support local
notifications directly in a future SDK release, at which point that support
should replace this delegate shim. No release timeline is currently committed.

## What This Reproduces

Issue #132 reports an iOS crash when `OneSignalSDK.DotNet` and
Expand All @@ -23,14 +28,18 @@ onesignalUserNotificationCenter:willPresentNotification:withCompletionHandler:

Both failures point at the same interaction: OneSignal's native iOS SDK swizzles
`UNUserNotificationCenterDelegate` methods, while `Plugin.LocalNotification`
installs its own delegate that only implements the normal Apple selectors.
installs its own delegate that only implements the normal Apple selectors. This
sample registers a small iOS delegate shim that exports the OneSignal-prefixed
selectors and overrides the plugin's normal handlers. Remote pushes are shown as
foreground banners; local notifications keep the plugin's behavior.

## Run

From this directory, run iOS:

```sh
cp .env.example .env
# Set ONESIGNAL_APP_ID in .env before launching.
./run-ios.sh
```

Expand All @@ -50,16 +59,29 @@ devices.
3. Tap `Request LocalNotification Permission`.
4. Tap `Show Local Notification` while the app is foregrounded.
5. If the notification is delivered, tap it from Notification Center.
6. Watch device logs for the `onesignalUserNotificationCenter:*` selector crash.
6. Send a OneSignal push while the app is foregrounded, then send another while
the app is backgrounded and tap it from Notification Center.
7. Watch device logs to confirm the `onesignalUserNotificationCenter:*` selector
paths no longer crash.

To reproduce the original issue #132 crash, remove the
`OneSignalCompatibleNotificationDelegate` registration from `MauiProgram.cs` and
delete `Platforms/iOS/OneSignalCompatibleNotificationDelegate.cs`, then repeat
the iOS push steps.

The bundled app ID matches the main demo app's default ID when
`ONESIGNAL_APP_ID` is missing or still set to the placeholder. To test against a
different OneSignal app, set `ONESIGNAL_APP_ID` in `.env`.
Set `ONESIGNAL_APP_ID` in `.env` before running the sample. The app does not
fall back to a built-in OneSignal app id.

## Notes

The native OneSignal iOS SDK now documents disabling swizzling via
`OneSignal_disable_swizzling` and manually forwarding notification delegate
methods. This .NET binding currently does not expose the newer manual forwarding
APIs, so this sample keeps swizzling enabled and demonstrates the reported
interaction directly.
APIs, so this sample keeps swizzling enabled and uses the delegate shim as a
local compatibility workaround.

Because OneSignal's swizzle exchanges implementations on the shim class, the
normal delegate callbacks bypass OneSignal's own notification processing. With
this workaround, OneSignal iOS foreground lifecycle events (`WillDisplay`,
`Clicked`) may not fire; the durable fix is exposing the manual forwarding APIs
in the binding.