Shiny Notifications
When to Use This Skill
Use this skill when the user needs to:
- Send local notifications (immediate, scheduled, repeating, or geofence-triggered)
- Manage notification channels with importance levels, sounds, and actions
- Handle notification tap responses and interactive action buttons
- Request notification permissions on iOS and Android
- Manage app icon badge counts
- Configure platform-specific notification behavior (Android ongoing, iOS subtitles, etc.)
Library Overview
| Item |
Value |
| NuGet Package |
Shiny.Notifications (iOS, Mac Catalyst, Android, macOS, Windows); Shiny.Notifications.Linux (Linux) |
| Primary Namespace |
Shiny.Notifications |
| Registration Namespace |
Shiny (extension methods on IServiceCollection) |
| Platforms |
iOS, Mac Catalyst, Android, macOS, Windows, Linux |
| Dependencies |
Shiny.Core, Shiny.Locations, Shiny.Support.Repositories |
Linux
Linux notifications ship in a separate package, Shiny.Notifications.Linux. They are delivered via the freedesktop org.freedesktop.Notifications D-Bus service (GNOME, KDE, XFCE, etc.) and support the same INotificationManager API surface as the other platforms. Scheduled notifications are tracked in-process only — there is no OS-level scheduler like BGTaskScheduler or WorkManager, so the host process must be running for a scheduled notification to fire. Channels are exposed but only a subset of freedesktop hints (urgency, category, image) are actually honoured by most daemons. Geofence triggers and time-sensitive flags are not applicable.
Register with services.AddNotifications<TDelegate>(); from the Shiny namespace — the same call site as the other platforms.
Setup
Register the notification services in your MauiProgram.cs:
using Shiny;
// Without a delegate (fire-and-forget notifications)
services.AddNotifications();
// With a delegate to handle notification taps
services.AddNotifications<MyNotificationDelegate>();
On iOS, you can optionally pass an IosConfiguration to control authorization and presentation options:
#if IOS || MACCATALYST
services.AddNotifications<MyNotificationDelegate>(new IosConfiguration(
UNAuthorizationOptions: UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,
PresentationOptions: UNNotificationPresentationOptions.Banner | UNNotificationPresentationOptions.Badge | UNNotificationPresentationOptions.Sound
));
#endif
Code Generation Instructions
When generating code that uses Shiny Notifications, follow these conventions:
Always request access before sending notifications:
var access = await notificationManager.RequestAccess();
if (access != AccessState.Available)
{
// Handle denied permission
return;
}
Use AccessRequestFlags when the notification uses triggers:
AccessRequestFlags.TimeSensitivity for scheduled or repeating notifications.
AccessRequestFlags.LocationAware for geofence-triggered notifications.
- Or use the
RequestRequiredAccess extension method that infers flags from the notification object.
A Notification must have a Message set -- validation will throw otherwise.
Only one trigger type per notification -- you cannot mix ScheduleDate, RepeatInterval, and Geofence on the same notification.
Implement INotificationDelegate for handling user taps:
public class MyNotificationDelegate : INotificationDelegate
{
public async Task OnEntry(NotificationResponse response)
{
// response.Notification -- the original notification
// response.ActionIdentifier -- which action button was pressed
// response.Text -- text reply if action was TextReply type
}
}
Create channels before sending notifications that reference them:
notificationManager.AddChannel(new Channel
{
Identifier = "alerts",
Importance = ChannelImportance.High,
Sound = ChannelSound.High
});
Use the convenience Send extension for simple notifications:
await notificationManager.Send("Title", "Message body");
For platform-specific properties, use the native subclasses:
- Android:
AndroidNotification and AndroidChannel
- iOS:
AppleNotification and AppleChannel
Always inject INotificationManager via constructor injection -- never create instances directly.
Use CancelScope wisely when cancelling:
CancelScope.DisplayedOnly -- clears only shown notifications.
CancelScope.Pending -- clears only scheduled/triggered notifications.
CancelScope.All -- clears everything (default).
Namespace Ambiguities
Notification: Both Shiny.Notifications and Shiny.Push define a Notification type. If both packages are referenced in the same project, do NOT add both namespaces as global usings. Use Shiny.Notifications.Notification FQN or a file-level using Shiny.Notifications; directive to disambiguate.
Best Practices
- Always check the
AccessState result before attempting to send notifications.
- Use channels to group notifications by category (e.g., "alerts", "reminders", "messages").
- The default channel (
Channel.Default) always exists with Identifier = "Notifications" and ChannelImportance.Low.
- Do not remove the default channel -- the library will throw an
InvalidOperationException.
- Set
BadgeCount only on immediate notifications (not triggered ones) -- validation will fail otherwise.
- Use
IntervalTrigger with either Interval (raw TimeSpan) or TimeOfDay (daily/weekly recurring), never both.
- For geofence notifications, ensure
Center and Radius are both set on GeofenceTrigger.
- On Android, create a drawable resource named
notification for the default small icon, or set SmallIconResourceName on AndroidNotification.
- Prefer the
RequestRequiredAccess extension method to automatically determine needed permission flags from a Notification object.
- Use
Payload dictionary on Notification to pass custom data that you can read back in your INotificationDelegate.OnEntry.
AI Tool Integration (Shiny.Notifications.Extensions.AI)
The optional Shiny.Notifications.Extensions.AI package exposes INotificationManager as reminder-framed Microsoft.Extensions.AI tool functions (AIFunctions) for LLM agents. You opt-in exactly which operations the model can see — a read/write allow-list you control on behalf of the agent (not an OS permission prompt; the platform notification permission must already be granted). Read-only by default; write is opt-in. AOT-compatible (hand-built schemas, JsonNode results — no reflection).
using Shiny.Notifications;
using Shiny.Notifications.Extensions.AI;
builder.Services.AddNotifications(); // registers INotificationManager
builder.Services.AddNotificationAITools(tools => tools
.AddReminders(ReminderAICapabilities.ReadWrite, channel: "reminders") // channel is optional
);
// resolve the bundle and pass the tools to any IChatClient
var tools = sp.GetRequiredService<NotificationAITools>().Tools;
var response = await chatClient.GetResponseAsync(
messages,
new ChatOptions { Tools = [.. tools] }
);
Key types:
AddNotificationAITools(Action<INotificationAIToolBuilder>) — DI extension; throws if nothing is added.
INotificationAIToolBuilder — AddReminders(ReminderAICapabilities, string? channel = null). The channel (if supplied) must already be registered via AddChannel.
ReminderAICapabilities [Flags] — None, Read (default), Write, ReadWrite.
NotificationAITools — resolve from DI; .Tools is IReadOnlyList<AITool>.
Generated tools (only for opted-in capabilities): list_reminders (pending/scheduled), create_reminder (omit both scheduleFor/repeatDailyAt to send now, scheduleFor for a one-time reminder, repeatDailyAt "HH:mm" for a daily one), cancel_reminder (by id). scheduleFor and repeatDailyAt are mutually exclusive; dates are ISO-8601.
The AI tools assume permissions are already granted — they do not trigger the platform permission UI (needs a foreground activity). Call INotificationManager.RequestAccess(...) from the app before invoking the agent.
Reference Files
1---2name: shiny-notifications3description: Cross-platform local notification management for .NET MAUI apps using Shiny, supporting scheduled, repeating, and geofence-triggered notifications with channels, badges, and interactive actions.4---56# Shiny Notifications78## When to Use This Skill910Use this skill when the user needs to:1112- Send local notifications (immediate, scheduled, repeating, or geofence-triggered)13- Manage notification channels with importance levels, sounds, and actions14- Handle notification tap responses and interactive action buttons15- Request notification permissions on iOS and Android16- Manage app icon badge counts17- Configure platform-specific notification behavior (Android ongoing, iOS subtitles, etc.)1819## Library Overview2021| Item | Value |22|---|---|23| **NuGet Package** | `Shiny.Notifications` (iOS, Mac Catalyst, Android, macOS, Windows); `Shiny.Notifications.Linux` (Linux) |24| **Primary Namespace** | `Shiny.Notifications` |25| **Registration Namespace** | `Shiny` (extension methods on `IServiceCollection`) |26| **Platforms** | iOS, Mac Catalyst, Android, macOS, Windows, Linux |27| **Dependencies** | `Shiny.Core`, `Shiny.Locations`, `Shiny.Support.Repositories` |2829### Linux3031Linux notifications ship in a separate package, `Shiny.Notifications.Linux`. They are delivered via the freedesktop `org.freedesktop.Notifications` D-Bus service (GNOME, KDE, XFCE, etc.) and support the same `INotificationManager` API surface as the other platforms. Scheduled notifications are tracked **in-process only** — there is no OS-level scheduler like BGTaskScheduler or WorkManager, so the host process must be running for a scheduled notification to fire. Channels are exposed but only a subset of freedesktop hints (urgency, category, image) are actually honoured by most daemons. Geofence triggers and time-sensitive flags are not applicable.3233Register with `services.AddNotifications<TDelegate>();` from the `Shiny` namespace — the same call site as the other platforms.3435## Setup3637Register the notification services in your `MauiProgram.cs`:3839```csharp40using Shiny;4142// Without a delegate (fire-and-forget notifications)43services.AddNotifications();4445// With a delegate to handle notification taps46services.AddNotifications<MyNotificationDelegate>();47```4849On iOS, you can optionally pass an `IosConfiguration` to control authorization and presentation options:5051```csharp52#if IOS || MACCATALYST53services.AddNotifications<MyNotificationDelegate>(new IosConfiguration(54 UNAuthorizationOptions: UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,55 PresentationOptions: UNNotificationPresentationOptions.Banner | UNNotificationPresentationOptions.Badge | UNNotificationPresentationOptions.Sound56));57#endif58```5960## Code Generation Instructions6162When generating code that uses Shiny Notifications, follow these conventions:63641. **Always request access before sending notifications:**65 ```csharp66 var access = await notificationManager.RequestAccess();67 if (access != AccessState.Available)68 {69 // Handle denied permission70 return;71 }72 ```73742. **Use `AccessRequestFlags` when the notification uses triggers:**75 - `AccessRequestFlags.TimeSensitivity` for scheduled or repeating notifications.76 - `AccessRequestFlags.LocationAware` for geofence-triggered notifications.77 - Or use the `RequestRequiredAccess` extension method that infers flags from the notification object.78793. **A `Notification` must have a `Message` set** -- validation will throw otherwise.80814. **Only one trigger type per notification** -- you cannot mix `ScheduleDate`, `RepeatInterval`, and `Geofence` on the same notification.82835. **Implement `INotificationDelegate` for handling user taps:**84 ```csharp85 public class MyNotificationDelegate : INotificationDelegate86 {87 public async Task OnEntry(NotificationResponse response)88 {89 // response.Notification -- the original notification90 // response.ActionIdentifier -- which action button was pressed91 // response.Text -- text reply if action was TextReply type92 }93 }94 ```95966. **Create channels before sending notifications that reference them:**97 ```csharp98 notificationManager.AddChannel(new Channel99 {100 Identifier = "alerts",101 Importance = ChannelImportance.High,102 Sound = ChannelSound.High103 });104 ```1051067. **Use the convenience `Send` extension for simple notifications:**107 ```csharp108 await notificationManager.Send("Title", "Message body");109 ```1101118. **For platform-specific properties, use the native subclasses:**112 - Android: `AndroidNotification` and `AndroidChannel`113 - iOS: `AppleNotification` and `AppleChannel`1141159. **Always inject `INotificationManager`** via constructor injection -- never create instances directly.11611710. **Use `CancelScope` wisely when cancelling:**118 - `CancelScope.DisplayedOnly` -- clears only shown notifications.119 - `CancelScope.Pending` -- clears only scheduled/triggered notifications.120 - `CancelScope.All` -- clears everything (default).121122## Namespace Ambiguities123124- **`Notification`**: Both `Shiny.Notifications` and `Shiny.Push` define a `Notification` type. If both packages are referenced in the same project, do NOT add both namespaces as global usings. Use `Shiny.Notifications.Notification` FQN or a file-level `using Shiny.Notifications;` directive to disambiguate.125126## Best Practices127128- Always check the `AccessState` result before attempting to send notifications.129- Use channels to group notifications by category (e.g., "alerts", "reminders", "messages").130- The default channel (`Channel.Default`) always exists with `Identifier = "Notifications"` and `ChannelImportance.Low`.131- Do not remove the default channel -- the library will throw an `InvalidOperationException`.132- Set `BadgeCount` only on immediate notifications (not triggered ones) -- validation will fail otherwise.133- Use `IntervalTrigger` with either `Interval` (raw TimeSpan) or `TimeOfDay` (daily/weekly recurring), never both.134- For geofence notifications, ensure `Center` and `Radius` are both set on `GeofenceTrigger`.135- On Android, create a drawable resource named `notification` for the default small icon, or set `SmallIconResourceName` on `AndroidNotification`.136- Prefer the `RequestRequiredAccess` extension method to automatically determine needed permission flags from a `Notification` object.137- Use `Payload` dictionary on `Notification` to pass custom data that you can read back in your `INotificationDelegate.OnEntry`.138139## AI Tool Integration (Shiny.Notifications.Extensions.AI)140141The optional `Shiny.Notifications.Extensions.AI` package exposes `INotificationManager` as **reminder-framed** `Microsoft.Extensions.AI` tool functions (`AIFunction`s) for LLM agents. You opt-in exactly which operations the model can see — a read/write allow-list you control on behalf of the agent (**not** an OS permission prompt; the platform notification permission must already be granted). Read-only by default; write is opt-in. AOT-compatible (hand-built schemas, `JsonNode` results — no reflection).142143```csharp144using Shiny.Notifications;145using Shiny.Notifications.Extensions.AI;146147builder.Services.AddNotifications(); // registers INotificationManager148builder.Services.AddNotificationAITools(tools => tools149 .AddReminders(ReminderAICapabilities.ReadWrite, channel: "reminders") // channel is optional150);151152// resolve the bundle and pass the tools to any IChatClient153var tools = sp.GetRequiredService<NotificationAITools>().Tools;154var response = await chatClient.GetResponseAsync(155 messages,156 new ChatOptions { Tools = [.. tools] }157);158```159160Key types:161- `AddNotificationAITools(Action<INotificationAIToolBuilder>)` — DI extension; throws if nothing is added.162- `INotificationAIToolBuilder` — `AddReminders(ReminderAICapabilities, string? channel = null)`. The channel (if supplied) must already be registered via `AddChannel`.163- `ReminderAICapabilities` `[Flags]` — `None`, `Read` (default), `Write`, `ReadWrite`.164- `NotificationAITools` — resolve from DI; `.Tools` is `IReadOnlyList<AITool>`.165166Generated tools (only for opted-in capabilities): `list_reminders` (pending/scheduled), `create_reminder` (omit both `scheduleFor`/`repeatDailyAt` to send now, `scheduleFor` for a one-time reminder, `repeatDailyAt` "HH:mm" for a daily one), `cancel_reminder` (by id). `scheduleFor` and `repeatDailyAt` are mutually exclusive; dates are ISO-8601.167168> The AI tools assume permissions are already granted — they do **not** trigger the platform permission UI (needs a foreground activity). Call `INotificationManager.RequestAccess(...)` from the app before invoking the agent.169170## Reference Files171172- [API Reference](reference/api-reference.md)