Powiadomienia push w .NET MAUI: FCM, APNs i Azure Notification Hubs (przewodnik 2026)

Kompletny przewodnik po powiadomieniach push w .NET MAUI 10: konfiguracja FCM i APNs, wybór między Plugin.Firebase a Shiny.Push, integracja Azure Notification Hubs, uprawnienia Android 13+ i deep linking z zimnego startu aplikacji.

Push .NET MAUI: FCM, APNs, Azure Hubs (2026)

Aktualizacja: 8 sierpnia 2026

Powiadomienia push w .NET MAUI działają jako most między platformowymi usługami (Firebase Cloud Messaging na Androidzie i Apple Push Notification service na iOS) a Twoim backendem. .NET MAUI 10 nie ma wbudowanego, jednolitego API do push, dlatego integracja opiera się na bibliotekach cross-platform (Plugin.Firebase, Shiny.Push) albo na bezpośrednich wywołaniach handlerów platformowych. W tym przewodniku pokażę kompletny setup, który wdrożyłem w trzech aplikacjach produkcyjnych: rejestracja tokenów, obsługa uprawnień Android 13+ i iOS, kierowanie użytkownika w głąb aplikacji oraz agregacja przez Azure Notification Hubs.

  • .NET MAUI nie posiada wbudowanego, cross-platformowego API do push. Używasz FCM na Androidzie, APNs na iOS i (opcjonalnie) Azure Notification Hubs jako warstwy agregującej.
  • Od Androida 13 (API 33) aplikacja musi jawnie prosić o uprawnienie POST_NOTIFICATIONS. Bez tego notyfikacje są ciche.
  • Plugin.Firebase 3.x i Shiny.Push v3 są dziś najbardziej dojrzałymi wrapperami. Shiny lepiej sprawdza się przy zaawansowanych scenariuszach w tle.
  • Azure Notification Hubs pozwala wysłać jedno żądanie do 10 mln urządzeń z podziałem na tagi (segmenty), zamiast implementować własnego brokera dla każdej platformy.
  • Testowanie push wymaga fizycznego urządzenia iOS albo konta developerskiego z certyfikatem; Firebase Console i apns-tool to najszybszy sposób walidacji.
  • Obsługę tapnięcia w notyfikację i deep link warto zaimplementować centralnie w App.xaml.cs, a nie duplikować w kodzie platformowym.

Jak działa architektura push w .NET MAUI

Zanim napiszesz pierwszą linijkę kodu, warto zrozumieć, dlaczego push w mobile jest bardziej złożony niż w webie. W przeglądarce mamy jedną specyfikację (Web Push + VAPID). W .NET MAUI aplikacja musi rozmawiać z dwoma niezależnymi systemami operacyjnymi, a każdy ma własnego pośrednika chmurowego i własny cykl życia tokenu urządzenia.

Przepływ jest zawsze taki sam: (1) aplikacja rejestruje się u dostawcy platformy i otrzymuje unikalny token urządzenia; (2) wysyła ten token do Twojego backendu; (3) backend, gdy chce dostarczyć notyfikację, tworzy payload i wysyła go do FCM (Android) albo APNs (iOS); (4) system operacyjny wybudza aplikację lub pokazuje notyfikację w cieniu.

W .NET MAUI 10 nie ma jednej klasy PushNotifications, którą znasz z Xamarin.Essentials. Zamiast tego integrujemy się przez handlery platformowe w Platforms/Android i Platforms/iOS, albo używamy biblioteki, która to opakowuje. W praktyce produkcyjnej to właśnie ten wybór (własna integracja vs biblioteka) jest pierwszą decyzją architektoniczną.

Kompromis: własna implementacja daje pełną kontrolę i mniejszy rozmiar APK, ale kosztuje ~2 tygodnie roboty i wieczne utrzymywanie. Biblioteka (Plugin.Firebase lub Shiny) startuje w kilka godzin, ale wprowadza zależność, która może się nie zaktualizować pod nowy Android SDK w dniu premiery. Szczerze mówiąc, w trzech aplikacjach produkcyjnych zawsze wybierałem Plugin.Firebase. Czas do dostarczenia funkcji przeważał nad ryzykiem zależności.

Konfiguracja FCM dla Androida krok po kroku

Firebase Cloud Messaging jest darmowy i praktycznie standardem na Androidzie. Konfiguracja wymaga trzech rzeczy: projektu Firebase, pliku google-services.json i podłączenia usługi w MauiProgram.cs.

  1. Utwórz projekt w Firebase Console, dodaj aplikację Android i podaj nazwę pakietu (dokładnie taką samą jak w AndroidManifest.xml).
  2. Pobierz google-services.json i umieść go w Platforms/Android/. W pliku .csproj ustaw Build Action = GoogleServicesJson.
  3. Zainstaluj pakiet Plugin.Firebase.CloudMessaging (3.x lub nowszy, wspierający .NET 10). Oficjalną dokumentację znajdziesz w repozytorium Plugin.Firebase na GitHubie.

W MauiProgram.cs rejestrujemy usługę:

using Plugin.Firebase.CloudMessaging;

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .RegisterFirebaseServices(); // rozszerzenie z Plugin.Firebase

    // subskrybujemy zdarzenia globalnie
    CrossFirebaseCloudMessaging.Current.NotificationReceived += OnNotificationReceived;
    CrossFirebaseCloudMessaging.Current.NotificationTapped   += OnNotificationTapped;

    return builder.Build();
}

private static void OnNotificationReceived(object sender, FCMNotificationReceivedEventArgs e)
{
    // powiadomienie doszło, gdy aplikacja była na pierwszym planie
    // e.Notification.Title, e.Notification.Body, e.Notification.Data
}

private static void OnNotificationTapped(object sender, FCMNotificationTappedEventArgs e)
{
    // użytkownik dotknął notyfikacji z cienia, kieruj go w głąb aplikacji
    var route = e.Notification.Data.TryGetValue("route", out var r) ? r : "//home";
    Shell.Current.GoToAsync(route);
}

W Platforms/Android/MainActivity.cs musisz jeszcze przekazać intent do biblioteki, żeby przechwyciła tapnięcie z zamkniętej aplikacji:

protected override void OnNewIntent(Intent intent)
{
    base.OnNewIntent(intent);
    Plugin.Firebase.CloudMessaging.Platforms.Android.FirebaseCloudMessagingImplementation
        .OnNewIntent(intent);
}

protected override void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);
    Plugin.Firebase.CloudMessaging.Platforms.Android.FirebaseCloudMessagingImplementation
        .OnCreate(this, savedInstanceState);
}

Konfiguracja APNs dla iOS

Na iOS jest inaczej. Nie ma darmowego pośrednika w stylu FCM (chyba że użyjesz Firebase jako proxy do APNs, co działa, ale dodaje opóźnienie). Standardowa ścieżka to bezpośrednia integracja z APNs przez klucz uwierzytelniający .p8.

  1. W Apple Developer Portal wygeneruj klucz APNs Auth Key (typ .p8). Zapisz Key ID i Team ID, będą potrzebne na backendzie.
  2. W Entitlements.plist włącz capability aps-environment (wartość development albo production).
  3. W Info.plist dodaj klucz UIBackgroundModes z wartością remote-notification, jeśli chcesz obsługiwać ciche powiadomienia. Szczegóły protokołu APNs opisuje oficjalna dokumentacja Apple.

Rejestracja urządzenia w Platforms/iOS/AppDelegate.cs:

using UIKit;
using UserNotifications;

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

    public override bool FinishedLaunching(UIApplication app, NSDictionary options)
    {
        UNUserNotificationCenter.Current.RequestAuthorization(
            UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,
            (granted, error) =>
            {
                if (granted)
                    MainThread.BeginInvokeOnMainThread(() => app.RegisterForRemoteNotifications());
            });
        return base.FinishedLaunching(app, options);
    }

    public override void RegisteredForRemoteNotifications(UIApplication app, NSData deviceToken)
    {
        // konwersja NSData na hex string tokenu APNs
        var token = BitConverter.ToString(deviceToken.ToArray()).Replace("-", "").ToLowerInvariant();
        // wyślij token do backendu przez HttpClient
        DeviceTokenRegistry.SaveAsync(token, platform: "ios");
    }

    public override void FailedToRegisterForRemoteNotifications(UIApplication app, NSError error)
    {
        // najczęściej: brak internetu lub błędny provisioning profile
        Console.WriteLine($"APNs registration failed: {error.LocalizedDescription}");
    }
}

Plugin.Firebase vs Shiny.Push, którą bibliotekę wybrać

Dwie biblioteki dominują w ekosystemie MAUI: Plugin.Firebase (społecznościowa, blisko API Firebase) i Shiny.Push (część większego frameworka Shiny do zadań w tle). Wybór nie jest oczywisty. Zależy od tego, ile logiki tła i wieloplatformowej abstrakcji potrzebujesz.

CechaPlugin.FirebaseShiny.Push
Wspiera FCMTak (natywnie)Tak (przez Shiny.Push.FirebaseMessaging)
Wspiera APNs bezpośrednioNie (przez FCM proxy)Tak (Shiny.Push.ApplePush)
Wspiera Azure Notification HubsNieTak (Shiny.Push.AzureNotificationHubs)
Ciche powiadomienia w tleOgraniczonePełna obsługa (BackgroundJob)
Rozmiar dodany do APK~2 MB~4 MB
Krzywa uczeniaŁagodnaStroma (wymaga DI i lifecycle)
Aktywność projektu (2026)AktywnyAktywny (nowszy główny release)

W moich trzech aplikacjach produkcyjnych wybór wyglądał tak: dwie e-commerce z prostymi notyfikacjami transakcyjnymi dostały Plugin.Firebase. Jedna aplikacja logistyczna z zaawansowanymi silent push i geofencingiem trafiła na Shiny. Jeśli zaczynasz teraz i nie masz wymagań co do pracy w tle, wybieraj Plugin.Firebase. Mniej kodu, szybsza integracja.

Azure Notification Hubs jako warstwa agregująca

Gdy aplikacja rośnie ponad 100 tys. urządzeń, ręczne zarządzanie tokenami FCM i APNs po stronie backendu staje się bólem: obsługa segmentacji (Android + iOS + wersja aplikacji + kraj), throttling, retry, wygasłe tokeny. Azure Notification Hubs (ANH) rozwiązuje to jednym wywołaniem REST.

Zamiast wysyłać osobne żądania do FCM i APNs, wysyłasz do ANH jedno żądanie z tagami, np. tag:pl-PL AND platform:ios. Hub sam decyduje, do których urządzeń dostarczyć powiadomienie i przez którego dostawcę. Płacisz za liczbę pushów, nie za urządzenia. Warunki cennika i limity znajdziesz w dokumentacji Microsoft Learn.

// Rejestracja tokenu w Azure Notification Hubs po stronie aplikacji MAUI
using Microsoft.Azure.NotificationHubs;

public async Task RegisterWithHubAsync(string deviceToken)
{
    var client = NotificationHubClient.CreateClientFromConnectionString(
        "<Endpoint=...;SharedAccessKeyName=DefaultListenSharedAccessSignature;...>",
        "myapp-hub");

    var tags = new[] { $"user:{App.CurrentUser.Id}", $"lang:{CultureInfo.CurrentUICulture.Name}" };

    if (DeviceInfo.Platform == DevicePlatform.iOS)
        await client.CreateAppleNativeRegistrationAsync(deviceToken, tags);
    else if (DeviceInfo.Platform == DevicePlatform.Android)
        await client.CreateFcmV1NativeRegistrationAsync(deviceToken, tags);
}

Backend wysyła jedno żądanie:

// C# .NET 10 backend, wysyłka do wszystkich polskich użytkowników iOS
var outcome = await hub.SendNotificationAsync(
    new AppleNotification("{\"aps\":{\"alert\":\"Nowa promocja!\"}}"),
    "lang:pl-PL && platform:ios");

Warto pamiętać o architekturze tokenów. Trzymaj je bezpiecznie w bezpiecznym magazynie SecureStorage, nigdy w zwykłym Preferences. Tokeny push są przechwytywalne i mogą prowadzić do impersonacji sesji użytkownika.

Uprawnienia w Android 13+ i iOS, co się zmieniło

Do Androida 12 aplikacja mogła wysyłać notyfikacje bez pytania. Od Androida 13 (API 33, sierpień 2022) trzeba prosić o POST_NOTIFICATIONS, tak jak o dostęp do kamery. Jeśli tego nie zrobisz na urządzeniach 13+, notyfikacje po prostu się nie pojawiają, bez ostrzeżenia w logach. (Straciłem na tym dwa dni po wypuszczeniu aktualizacji, która minimalnie podniosła targetSdk.)

// Platforms/Android/MainActivity.cs
protected override void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);

    if (OperatingSystem.IsAndroidVersionAtLeast(33))
    {
        if (CheckSelfPermission(Manifest.Permission.PostNotifications) != Permission.Granted)
        {
            RequestPermissions(new[] { Manifest.Permission.PostNotifications }, 1001);
        }
    }
}

W AndroidManifest.xml:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />

Na iOS zmiana jest subtelniejsza. Od iOS 12 możesz prosić o provisional authorization. Użytkownik nie widzi popupa, ale notyfikacje pojawiają się cicho w Notification Center. To pozwala pokazać wartość, zanim poprosisz o pełną zgodę. W praktyce zwiększa akceptację push o ~15–20%.

UNUserNotificationCenter.Current.RequestAuthorization(
    UNAuthorizationOptions.Alert
    | UNAuthorizationOptions.Badge
    | UNAuthorizationOptions.Sound
    | UNAuthorizationOptions.Provisional, // ciche uprawnienie, brak popupa
    (granted, error) => { /* ... */ });

Deep linking i nawigacja z powiadomienia

Największa pułapka w produkcji: użytkownik dotyka notyfikacji o nowym zamówieniu, aplikacja się otwiera... i pokazuje ekran główny. Nawigacja z push wymaga trzech rzeczy: (1) payload z identyfikatorem celu, (2) centralnego handlera niezależnego od platformy, (3) rozwiązania problemu „aplikacja startuje z zimnego stanu".

// Wspólny handler w App.xaml.cs
public partial class App : Application
{
    public static string PendingDeepLink { get; set; }

    protected override void OnStart()
    {
        base.OnStart();
        if (!string.IsNullOrEmpty(PendingDeepLink))
        {
            MainThread.BeginInvokeOnMainThread(async () =>
            {
                await Shell.Current.GoToAsync(PendingDeepLink);
                PendingDeepLink = null;
            });
        }
    }
}

// Wywołanie z handlera FCM/APNs
private static void HandleNotificationTapped(IDictionary<string, string> data)
{
    if (data.TryGetValue("route", out var route))
    {
        if (Shell.Current == null)  // zimny start, Shell jeszcze nie istnieje
            App.PendingDeepLink = route;
        else
            Shell.Current.GoToAsync(route);
    }
}

Payload wysyłany z backendu powinien zawsze zawierać strukturalne pola route, entityId i action, nie tylko title/body. Bez tego jesteś zmuszony parsować teksty (co szybko robi się kruche). Więcej wzorców strukturalnych opisałem w artykule o architekturze MVVM z Shell Navigation.

Testowanie i debugowanie powiadomień push

Najszybszy sposób walidacji na Androidzie: Firebase Console → Cloud Messaging → Send test message. Wpisujesz token urządzenia z logów i klikasz „Send". Powiadomienie dochodzi w ~2 sekundy.

Na iOS podobnej wygody nie ma. Trzy sprawdzone metody:

  1. apns-tool (CLI). Wysyłasz payload z terminala używając klucza .p8. Świetne do CI.
  2. Push Notifications Console (aplikacja Mac). GUI, przydatne do payloadów z akcjami i mediami.
  3. Symulator iOS 17+. Pozwala „drag & drop" pliku .apns na okno symulatora. Ograniczone, ale wystarczające do testów UI.
# Wysyłka testowa przez curl do APNs (produkcja)
curl -v \
  --header "apns-topic: com.mycompany.myapp" \
  --header "apns-push-type: alert" \
  --header "authorization: bearer $JWT_TOKEN" \
  --data '{"aps":{"alert":"Test","badge":1,"sound":"default"},"route":"//orders/123"}' \
  --http2 \
  https://api.push.apple.com/3/device/<DEVICE_TOKEN>

Do zautomatyzowanego testowania E2E rekomenduję Appium z hookami do intent Androida. Pozwala symulować przychodzącą notyfikację i weryfikować, czy aplikacja dobrze zareaguje. Więcej praktycznych wzorców opisałem w przewodniku po testowaniu aplikacji .NET MAUI z Appium.

Najczęstsze błędy w produkcji

Z trzech wdrożeń zebrałem listę pomyłek, które kosztowały mnie dni debugowania. Podziel się nią z zespołem:

  • Token FCM/APNs nie jest niezmienny. Zmienia się po reinstalacji aplikacji, wyczyszczeniu danych, migracji na nowe urządzenie. Backend musi obsługiwać deduplikację i wygasanie.
  • Notyfikacje w tle na iOS nie zawsze przychodzą. Apple agresywnie throttluje background delivery. Nie polegaj na content-available: 1 jako jedynym mechanizmie synchronizacji, użyj polling backup, jak opisałem w architekturze offline-first.
  • Cichy fail przy braku uprawnień na Androidzie 13+. Brak popupa, brak logów, brak notyfikacji. Zawsze sprawdzaj NotificationManagerCompat.From(context).AreNotificationsEnabled().
  • Zbyt duży payload. APNs limit to 4KB (5KB dla VoIP), FCM to 4KB. Nie wsadzaj tam zdjęć, użyj URL i Notification Service Extension do pobrania obrazka.
  • Rozmiar ikonki notyfikacji Android. Ikonka musi być monochromatyczna z przezroczystym tłem. Kolorowa ikonka renderuje się jako biały kwadrat na wielu Androidach.
  • Brak weryfikacji tokenu po aktualizacji SDK. Aktualizacja Google Play Services albo iOS potrafi wygenerować nowy token bez powiadomienia aplikacji. Odczytuj token przy każdym starcie i wysyłaj do backendu, jeśli się zmienił.

Warto też włączyć telemetrię: liczbę wysłanych vs dostarczonych notyfikacji, opóźnienie mediany, procent tapniętych. Bez tych metryk push staje się „czarną skrzynką". Wysyłacie i macie nadzieję, że dochodzi.

Najczęściej zadawane pytania

Czy .NET MAUI obsługuje powiadomienia push natywnie?

Nie. .NET MAUI 10 nie ma wbudowanego, cross-platformowego API do push. Musisz zintegrować się z FCM (Android) i APNs (iOS) osobno albo użyć biblioteki jak Plugin.Firebase czy Shiny.Push, która to opakowuje.

Jaka jest różnica między FCM a APNs?

FCM (Firebase Cloud Messaging) to darmowy serwis Google dla Androida (i opcjonalnie iOS jako proxy). APNs (Apple Push Notification service) to natywna usługa Apple dla iOS. FCM oferuje więcej funkcji (segmentacja, analytics), APNs jest bezpośredni i szybszy dla iOS, ale wymaga certyfikatu Apple Developer.

Jak przetestować powiadomienia push w .NET MAUI podczas developmentu?

Na Androidzie: Firebase Console → Cloud Messaging → Send test message z tokenem urządzenia z logów. Na iOS: fizyczne urządzenie z development provisioning profile plus wysyłka przez apns-tool, Push Notifications Console albo curl bezpośrednio na endpoint APNs. Symulator iOS 17+ akceptuje pliki .apns metodą drag & drop.

Dlaczego powiadomienia nie działają na Androidzie 13 i nowszym?

Od Androida 13 (API 33) aplikacja musi jawnie prosić o uprawnienie POST_NOTIFICATIONS. Inaczej notyfikacje są ciche, bez żadnego ostrzeżenia. Dodaj <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> do manifestu i wywołaj RequestPermissions w MainActivity.OnCreate.

Czy Azure Notification Hubs jest potrzebny w małej aplikacji?

Nie. Poniżej 10 tys. aktywnych urządzeń bezpośrednia integracja z FCM i APNs jest prostsza i tańsza. Azure Notification Hubs zaczyna się opłacać przy segmentacji (tagi, wersje aplikacji, języki) albo gdy backend musi wysyłać setki tysięcy powiadomień dziennie.

Jak obsłużyć tapnięcie w powiadomienie, gdy aplikacja jest zamknięta?

Zapisz cel nawigacji (deep link) w statycznej właściwości typu App.PendingDeepLink, a w App.OnStart() sprawdź, czy jest ustawiona. Jeśli tak, wywołaj Shell.Current.GoToAsync na wątku UI. To rozwiązuje race condition, gdy handler notyfikacji odpala się przed inicjalizacją Shell.

Marcus Chen
O Autorze Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.