Actualizat: 2 august 2026
Push notifications în .NET MAUI se implementează folosind Firebase Cloud Messaging (FCM) pentru Android și Apple Push Notification service (APNs) pentru iOS, integrate printr-un plugin unificat precum Plugin.Firebase.CloudMessaging, care oferă un API cross-platform peste ambele. În .NET MAUI 9 și 10 fluxul standard include înregistrarea unui token per dispozitiv, cererea explicită de permisiuni (obligatorie pe iOS 18+ și Android 13+) și un handler comun pentru mesajele primite. Ghidul de față parcurge întreg fluxul, cu cod real testat în producție pe două proiecte pe care le-am livrat în ultimele 12 luni.
Cum funcționează push notifications în .NET MAUI?
Ok, hai să pornim de la bază. O notificare push traversează trei sisteme independente: backend-ul aplicației, un serviciu de messaging (FCM sau APNs) și dispozitivul destinatar. Backend-ul nu contactează niciodată direct telefonul. În schimb, trimite mesajul către FCM/APNs, iar sistemul de operare al dispozitivului decide dacă afișează notificarea, o rutează către o aplicație în background sau o livrează unui handler când aplicația este activă.
În .NET MAUI, arhitectura recomandată în 2026 folosește Firebase Cloud Messaging ca strat unic pentru ambele platforme. Firebase acționează ca proxy către APNs pe iOS: trimiți mesajul către FCM cu un topic sau token, iar Google îl împachetează în formatul APNs și îl livrează prin infrastructura Apple. Abordarea asta reduce cantitatea de cod platformă-specific pe care trebuie s-o menții și centralizează analiticele.
Fluxul de bază pentru fiecare dispozitiv arată așa: aplicația cere permisiunea utilizatorului, plugin-ul negociază cu FCM/APNs pentru a obține un registration token unic per instalare, tokenul e trimis către backend și stocat, iar când vrei să notifici un utilizator, backend-ul cheamă API-ul FCM HTTP v1 cu tokenul respectiv. Deoarece tokenii se pot roti (reinstalare, restore, ștergere aplicație pe iCloud), backend-ul trebuie să suporte actualizarea lor și eliminarea celor invalidați cu răspuns UNREGISTERED.
Configurarea Firebase Cloud Messaging pentru Android
Începe prin a crea un proiect Firebase la console.firebase.google.com. După crearea proiectului, adaugă o aplicație Android și introdu ApplicationId-ul din .csproj ca package name (de exemplu com.mobiletechlead.demoapp). Descarcă fișierul google-services.json generat.
Plasează fișierul în Platforms/Android/google-services.json și marchează-l ca GoogleServicesJson în .csproj:
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<GoogleServicesJson Include="Platforms\Android\google-services.json" />
</ItemGroup>Actualizează apoi Platforms/Android/AndroidManifest.xml cu permisiunile necesare pentru Android 13+:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
</manifest>Pentru iconițe și canale de notificări (obligatorii pe Android 8+), adaugă un icon monocrom în Resources/drawable/ic_stat_notification.png și înregistrează un canal implicit la pornirea aplicației. Canalul influențează grupul de setări afișat utilizatorului și trebuie să aibă un ID stabil între versiuni. Dacă îl schimbi, utilizatorii pierd preferințele configurate anterior (am pățit-o odată la un rebranding și a fost neplăcut de explicat clientului).
Configurarea APNs pentru iOS
Pe iOS, autentificarea către Apple Push Notification service se face în 2026 aproape exclusiv cu chei .p8, adică un fișier cu o cheie privată ECDSA pe care Firebase o folosește pentru a genera JWT-uri semnate. Certificatele .p12 mai sunt suportate, dar cheia .p8 nu expiră și acoperă toate aplicațiile din contul tău de developer. Detaliile complete sunt în documentația oficială Apple pentru remote notifications.
Generează cheia în Apple Developer » Keys, activând opțiunea Apple Push Notifications service (APNs). Notează Key ID și Team ID; vei avea nevoie de ambele. Fișierul .p8 se descarcă o singură dată, deci fă-i backup într-un secret manager (1Password, GitHub secrets, Azure Key Vault).
În consola Firebase, deschide setările proiectului » Cloud Messaging » Apple app configuration și încarcă cheia .p8 împreună cu Key ID și Team ID. Fără acest pas, mesajele către iOS eșuează silențios cu status 401 în răspunsul FCM, iar plugin-ul nu raportează erori la dispozitiv. Da, e o experiență frustrantă de depanat.
Adaugă capabilitățile în Platforms/iOS/Entitlements.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
<key>com.apple.developer.usernotifications.filtering</key>
<true/>
</dict>
</plist>În Info.plist, adaugă UIBackgroundModes cu remote-notification pentru a permite livrarea în background:
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>Pentru mai multe detalii despre configurația App Store, capabilities și App IDs, aruncă o privire peste ghidul CI/CD pentru .NET MAUI cu GitHub Actions. Acolo tratez integrarea semnării iOS în pipeline, care e partea care de obicei pică prima în producție.
Instalare și inițializare Plugin.Firebase.CloudMessaging
Ecosistemul .NET MAUI oferă mai multe pachete, dar cel mai matur în 2026 rămâne Plugin.Firebase, întreținut de TobiasBuchholz (vezi repository-ul oficial pe GitHub). Instalează pachetele pentru core și messaging:
dotnet add package Plugin.Firebase --version 3.2.0
dotnet add package Plugin.Firebase.CloudMessaging --version 3.2.0Modifică MauiProgram.cs pentru a inițializa Firebase la startup. E important să apelezi CreateDefaultInstance înainte de orice altă chemare Firebase, altfel plugin-ul aruncă NullReferenceException pe primul mesaj (am pierdut vreo două seri până am prins asta prima dată):
using Plugin.Firebase.Bundled.Shared;
using Plugin.Firebase.CloudMessaging;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.RegisterFirebaseServices();
return builder.Build();
}
private static MauiAppBuilder RegisterFirebaseServices(this MauiAppBuilder builder)
{
builder.ConfigureLifecycleEvents(events =>
{
#if IOS
events.AddiOS(iOS => iOS.FinishedLaunching(
(app, launchOptions) =>
{
CrossFirebase.Initialize();
return false;
}));
#elif ANDROID
events.AddAndroid(android => android.OnCreate(
(activity, _) => CrossFirebase.Initialize(activity)));
#endif
});
builder.Services.AddSingleton(_ => CrossFirebaseCloudMessaging.Current);
return builder;
}
}Pe Android, MainActivity trebuie să suprascrie OnNewIntent pentru a livra mesajul plugin-ului atunci când utilizatorul deschide aplicația prin tap pe notificare:
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
CrossFirebaseCloudMessaging.Current
.Notification.OnNotificationTapped(intent);
}Cerere de permisiuni și obținerea token-ului
Nu poți primi notificări fără permisiunea explicită a utilizatorului. Pe iOS a fost mereu obligatorie; pe Android a devenit obligatorie începând cu API 33. Am văzut mult prea des aplicații care afișează popup-ul de permisiune la primul launch. E o practică care duce direct la respingere. Cere permisiunea într-un context care justifică decizia (după onboarding, la activarea unei funcționalități opționale), nu în milisecunda 300 după splash screen.
public class NotificationService
{
private readonly IFirebaseCloudMessaging _fcm;
public NotificationService(IFirebaseCloudMessaging fcm) => _fcm = fcm;
public async Task<string?> TryRegisterAsync()
{
var granted = await _fcm.CheckIfValidAsync();
if (!granted)
{
var result = await _fcm.RequestNotificationPermissionAsync();
if (!result) return null;
}
await _fcm.SubscribeToTopicAsync("all-users");
var token = await _fcm.GetTokenAsync();
return token;
}
}Trimite tokenul obținut către backend împreună cu UserId și metadate utile pentru segmentare: Platform, AppVersion, Locale, DeviceModel. Stochează tokenul într-o tabelă separată de Users, indexată după Token, pentru că un utilizator poate avea multiple dispozitive. Când FCM returnează UNREGISTERED sau INVALID_ARGUMENT, șterge tokenul din DB. Trimiterile ulterioare vor fi refuzate oricum și îți consumă cotă.
Ascultă evenimentul TokenRefreshed pentru cazurile în care token-ul se rotește (backup restore, dezinstalare-reinstalare, curățare date):
_fcm.TokenChanged += async (_, e) =>
{
await SyncTokenToBackendAsync(e.Token);
};Trimiterea unei notificări din backend
Din iunie 2024, Google a retras complet API-ul legacy FCM (Server Key). Orice cod care mai folosește fcm.googleapis.com/fcm/send primește 404. Standardul actual este HTTP v1 cu autentificare OAuth2 și un service account JSON descărcat din Firebase Console » Project Settings » Service Accounts. Detaliile complete sunt în ghidul oficial de migrare Firebase.
Din backend-ul .NET, cel mai simplu mod de a trimite notificări este cu SDK-ul oficial FirebaseAdmin:
using FirebaseAdmin;
using FirebaseAdmin.Messaging;
using Google.Apis.Auth.OAuth2;
public class PushSender
{
public PushSender(string credentialsPath)
{
if (FirebaseApp.DefaultInstance is null)
{
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromFile(credentialsPath)
});
}
}
public async Task<string> SendAsync(
string token, string title, string body,
IDictionary<string, string>? data = null)
{
var message = new Message
{
Token = token,
Notification = new Notification { Title = title, Body = body },
Data = data,
Android = new AndroidConfig
{
Priority = Priority.High,
Notification = new AndroidNotification
{
ChannelId = "default_channel",
Sound = "default"
}
},
Apns = new ApnsConfig
{
Aps = new Aps
{
Sound = "default",
ContentAvailable = true,
MutableContent = true
}
}
};
return await FirebaseMessaging.DefaultInstance.SendAsync(message);
}
}Distincția între Notification și Data în payload este critică: câmpul Notification este afișat automat de sistem când aplicația e în background, iar Data ajunge întotdeauna la handler-ul aplicației. Pentru control total peste UI (personalizare, deep linking condițional, sincronizare tăcută), trimite doar Data și randează notificarea manual. Pe iOS însă, modul acesta necesită content-available: 1 și rămâne rate-limited de sistem.
Notificări în background și deep linking
Când un utilizator apasă pe o notificare, vrei să-l duci direct la conținutul relevant: un mesaj din chat, un comentariu, o pagină de detaliu comandă. Deep linking-ul se face prin combinarea payload-ului Data cu Shell.Current.GoToAsync. Am descris pattern-ul de navigație în detaliu în ghidul dedicat Shell Navigation; ideea aici este să delegi ruta către un handler central care rulează după inițializarea Shell-ului.
public partial class App : Application
{
private readonly IFirebaseCloudMessaging _fcm;
public App(IFirebaseCloudMessaging fcm)
{
InitializeComponent();
_fcm = fcm;
MainPage = new AppShell();
_fcm.NotificationTapped += OnNotificationTapped;
}
private async void OnNotificationTapped(object? sender, FCMNotificationTappedEventArgs e)
{
if (e.Notification.Data is null) return;
if (e.Notification.Data.TryGetValue("route", out var route)
&& !string.IsNullOrWhiteSpace(route))
{
await Shell.Current.GoToAsync(route);
}
}
}Backend-ul trimite pur și simplu data: { "route": "//home/orders/details?id=42" }, iar aplicația știe unde să navigheze. Testează întotdeauna trei stări distincte: aplicație închisă (cold start din notificare), aplicație în background și aplicație în foreground. Pe iOS, cold start din notificare pierde uneori evenimentul dacă handler-ul nu este atașat înainte de FinishedLaunching. Motivul: inițializarea Firebase trebuie să rămână în lifecycle-ul iOS și nu într-un serviciu lazy.
Pentru sincronizări tăcute (data-only messages fără UI vizibil), setează ContentAvailable = true pe iOS și omite Notification din mesaj. Aplicația primește un handler MessageReceived chiar și când e în background. Pe iOS însă, sistemul limitează frecvența la câteva mesaje pe oră per aplicație. Nu construi arhitecturi bazate pe garanția livrării imediate.
Depanare: probleme frecvente și soluții
Notificările nu ajung? În ordinea probabilităților, verifică:
- Token invalid sau expirat. Răspunsul FCM conține
error.code = UNREGISTERED. Soluție: rulează un job de curățare care șterge tokenurile la primul refuz. - Permisiuni refuzate. Pe Android 13+, uită-te în Settings » Apps » App » Notifications. Utilizatorul poate revoca oricând, iar plugin-ul nu detectează retroactiv acest lucru; verifică cu
CheckIfValidAsyncla fiecare foreground. - Cheia APNs configurată greșit în Firebase. Testează cu Firebase Console » Cloud Messaging » Send test message. Dacă mesajul ajunge pe Android dar nu pe iOS, cheia
.p8sau Team ID sunt greșite. - Rulezi pe simulator iOS. Simulatorul Xcode 15+ suportă push notifications, dar necesită un fișier
.apnsdrag-and-drop. Pentru testarea reală, folosește un dispozitiv fizic. - Aplicația este în stare Doze pe Android. Dacă utilizatorul a activat optimizarea bateriei, sistemul poate întârzia sau bloca mesajele cu prioritate normală. Folosește
Priority.Highpentru mesaje critice. - Build de producție folosește entitlements de development. Verifică
aps-environmentsă fieproduction, nudevelopment, în build-ul urcat pe App Store.
Pentru debug pe iOS, activează Debug logging în Console.app filtrat după subsystem:com.apple.pushLaunch. Vei vedea exact ce mesaje au ajuns la dispozitiv și dacă sistemul le-a livrat aplicației. Pe Android, adb logcat -s "FirebaseMessaging" arată tot fluxul de la primirea mesajului până la afișare.
Întrebări frecvente
.NET MAUI suportă nativ Firebase Cloud Messaging?
.NET MAUI nu include un binding oficial Microsoft pentru FCM, dar există pachete NuGet mature; Plugin.Firebase.CloudMessaging este standardul de facto în 2026. Alternativ, poți lega direct SDK-ul nativ Firebase per platformă folosind bindings-urile Android și iOS.
Care este diferența între notificări locale și push notifications?
Notificările locale sunt programate de aplicație pe dispozitiv (folosind UNUserNotificationCenter pe iOS sau AlarmManager pe Android) și nu implică server. Push notifications provin de la un backend care le trimite prin FCM sau APNs, chiar și când aplicația e închisă.
Pot trimite push notifications către iOS fără Firebase?
Da. Poți contacta APNs direct de pe backend folosind protocolul HTTP/2 și cheia .p8, ocolind Firebase. Este util pentru arhitecturi enterprise, dar pierzi centralizarea analiticelor și trebuie să implementezi separat livrarea pentru Android.
De ce nu primesc push notifications pe iOS Simulator?
Simulatorul Xcode nu se înregistrează la APNs în modul obișnuit. Pentru testare, generează un fișier .apns cu payload-ul dorit și trage-l peste fereastra simulatorului; Xcode îl livrează aplicației ca notificare falsă. Pentru testarea reală end-to-end folosește un dispozitiv fizic.
Cum implementez deep linking dintr-o notificare în .NET MAUI Shell?
Include o cheie route în payload-ul data al mesajului FCM (de exemplu "//home/orders/details?id=42"). În handler-ul NotificationTapped, extrage valoarea și apelează Shell.Current.GoToAsync(route). Asigură-te că evenimentul e atașat înainte ca Shell-ul să fie complet inițializat.




