Push Notifications în .NET MAUI 2026: Ghid FCM și APNs cu Cod Practic
Ghid practic de push notifications în .NET MAUI 9/10 cu Firebase Cloud Messaging și APNs: setup pas cu pas, cod C# testat în producție, permisiuni Android 13+ și deep linking prin Shell.
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.
FCM este stratul comun pentru Android, iar pe iOS FCM face pod peste APNs. Configurezi cheia APNs în consola Firebase pentru a livra mesaje pe iOS.
Plugin.Firebase.CloudMessaging (v3.x, iunie 2026) este cel mai stabil pachet pentru .NET MAUI 9/10 și abstractizează diferențele între platforme.
Începând cu Android 13 (API 33), permisiunea POST_NOTIFICATIONS este obligatorie și trebuie cerută în runtime, altfel notificările sunt tăcute silențios.
API-ul legacy FCM (Server Key) a fost retras în iunie 2024; backend-ul trebuie să folosească HTTP v1 cu OAuth2 și service account JSON.
Pentru deep linking dintr-o notificare, combină payload-ul data cu Shell.Current.GoToAsync într-un handler central.
Pe iOS, background push necesită capability-ul Remote notifications și un content-available flag setat pe mesaj.
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:
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:
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:
Modifică 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ă):
Pe Android, MainActivity trebuie să suprascrie OnNewIntent pentru a livra mesajul plugin-ului atunci când utilizatorul deschide aplicația prin tap pe notificare:
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):
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 CheckIfValidAsync la 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 .p8 sau Team ID sunt greșite.
Rulezi pe simulator iOS. Simulatorul Xcode 15+ suportă push notifications, dar necesită un fișier .apns drag-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.High pentru mesaje critice.
Build de producție folosește entitlements de development. Verifică aps-environment să fie production, nu development, î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.
Ghid practic 2026 pentru accesibilitatea .NET MAUI: SemanticProperties, VoiceOver, TalkBack, WCAG 2.2, Dynamic Type și target-uri tactile, cu exemple XAML gata de folosit pentru iOS și Android.
Ghid complet pentru custom handlers în .NET MAUI 2026: învață cum funcționează arhitectura pe două straturi, cum modifici mappers, cum construiești un handler nou pe iOS și Android și cum eviți memory leaks în producție.