Notifications Push dans .NET MAUI 10 : Guide Complet avec FCM et APNs (2026)

Tutoriel complet pour ajouter les notifications push dans .NET MAUI 10 : configuration FCM Android, APNs iOS avec clé .p8, deep linking Shell et gestion des états premier plan, arrière-plan et arrêté. Avec code C# prêt à copier.

Notifications Push MAUI 10 : Guide 2026

Mis à jour : 29 août 2026

Les notifications push dans .NET MAUI 10 ne sont pas prises en charge nativement par le framework : il faut brancher Firebase Cloud Messaging (FCM) côté Android et Apple Push Notification service (APNs) côté iOS, généralement via le paquet Plugin.Firebase.CloudMessaging. Concrètement, vous obtenez un jeton d'appareil par plateforme, vous l'envoyez à votre backend, puis vous gérez la réception au premier plan, en arrière-plan et à froid. Le reste de ce guide couvre la configuration bout en bout, y compris les pièges spécifiques à iOS 18 et Android 15.

  • .NET MAUI 10 n'expose aucune API de notifications push par défaut. Le duo Plugin.Firebase.CloudMessaging 3.x plus entitlements Apple est la voie standard en 2026.
  • Depuis Android 13 (API 33), la permission POST_NOTIFICATIONS est obligatoire et se demande à l'exécution, pas dans le manifeste seul.
  • Sur iOS, l'authentification APNs par clé .p8 (JWT) a remplacé les certificats .p12 depuis novembre 2025. Ne perdez plus de temps avec les vieux tutoriels.
  • Les notifications « silencieuses » (content-available: 1) sont fortement limitées par iOS. Apple applique un throttling agressif si vous en abusez.
  • Le deep linking depuis une notification passe par la même infrastructure que Shell.Current.GoToAsync, et doit être testé sur les trois états : premier plan, arrière-plan, arrêté.
  • Testez toujours sur un appareil physique iOS. Le simulateur ne signale pas certains bugs APNs de production.

Pourquoi les notifications push dans MAUI sont-elles si compliquées ?

Parce que « push » n'est pas une chose : ce sont deux protocoles totalement différents empilés derrière une abstraction que .NET MAUI ne fournit pas. Côté Android, les messages transitent par Firebase Cloud Messaging, un service Google qui gère lui-même la connexion socket HTTP/2 vers l'appareil. Côté iOS, c'est Apple Push Notification service, un canal TLS chiffré qu'Apple contrôle de bout en bout. Les charges utiles n'ont pas la même forme, les jetons ne sont pas au même format, les règles de priorité sont incompatibles, et les stratégies de collapsing (fusion de notifications) divergent.

MAUI 10 vous laisse cette plomberie à faire vous-même.

Vous avez trois options : appeler directement les SDK natifs via [SupportedOSPlatform], utiliser un plugin communautaire, ou déléguer à un service tiers (OneSignal, Azure Notification Hubs, Amazon SNS). En pratique, la voie la plus courte et la moins fragile en 2026, c'est Plugin.Firebase.CloudMessaging, qui unifie les deux plateformes en une API C#. À condition d'accepter que FCM route aussi vos messages iOS via ses serveurs, ce qui reste la recommandation officielle de Google depuis le retrait de l'API HTTP legacy en juin 2024.

Note importante : ne confondez pas notifications push (envoyées par un serveur, reçues via APNs/FCM) et notifications locales (planifiées par l'app elle-même). Ces dernières utilisent UNUserNotificationCenter sur iOS et NotificationManagerCompat sur Android, sans aucun serveur. Ce guide se concentre sur le premier cas.

Configurer Firebase Cloud Messaging pour Android

Rendez-vous sur la console Firebase, créez un projet, puis ajoutez une application Android avec le package name exact de votre projet MAUI (défini dans ApplicationId de votre .csproj). Téléchargez le fichier google-services.json.

Placez ce fichier dans Platforms/Android/, puis déclarez-le comme GoogleServicesJson dans le .csproj. C'est l'étape que 90 % des tutoriels sautent, et j'ai passé toute une soirée à déboguer ça sur un projet client l'an dernier :

<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
  <GoogleServicesJson Include="Platforms\Android\google-services.json" />
</ItemGroup>

Ajoutez ensuite la permission POST_NOTIFICATIONS dans Platforms/Android/AndroidManifest.xml. Depuis Android 13 (API 33), le simple fait de la déclarer ne suffit plus. Vous devrez aussi la demander à l'exécution, comme pour la caméra ou la localisation :

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

Pour les canaux de notification (obligatoires depuis Android 8), créez-les dès le démarrage dans MainActivity.OnCreate. Chaque canal a son propre son, sa propre importance et son propre statut « ne pas déranger ». C'est l'utilisateur qui les contrôle depuis les paramètres système, pas vous.

Configurer APNs pour iOS avec la clé .p8

Sur le portail développeur Apple, section Keys, générez une nouvelle clé avec la capacité APNs. Vous obtenez un fichier .p8 (à télécharger une seule fois, Apple ne le regénère pas) et un Key ID. Notez aussi votre Team ID, visible en haut à droite du portail.

Dans la console Firebase, ouvrez Project Settings → Cloud Messaging → Apple app configuration. Téléversez le .p8, le Key ID et le Team ID. C'est Firebase qui signe désormais les JWT APNs à votre place, ce qui vous évite de recharger un certificat tous les ans.

Côté projet MAUI, activez les capabilities Push Notifications et Background Modes → Remote notifications dans Platforms/iOS/Entitlements.plist :

<key>aps-environment</key>
<string>development</string>
<key>UIBackgroundModes</key>
<array>
  <string>remote-notification</string>
  <string>fetch</string>
</array>

N'oubliez pas de basculer aps-environment sur production pour vos builds TestFlight/App Store. J'ai perdu littéralement une demi-journée sur ce point : un provisioning profile de production avec la valeur development génère silencieusement des jetons invalides, et vous vous demanderez pendant des heures pourquoi vos push ne partent pas. C'est le piège APNs classique.

Placez également votre GoogleService-Info.plist (téléchargé depuis Firebase) dans Platforms/iOS/ avec l'action de build BundleResource.

Installer et brancher Plugin.Firebase dans MAUI 10

Le paquet Plugin.Firebase.CloudMessaging, maintenu par Tobias Buchholz, est la référence communautaire en 2026 (version 3.1+ compatible .NET 10). Ajoutez-le à votre projet MAUI :

dotnet add package Plugin.Firebase.CloudMessaging --version 3.1.1
dotnet add package Plugin.Firebase.Core --version 3.1.1

Dans MauiProgram.cs, initialisez Firebase au démarrage. La méthode diffère selon la plateforme. Android lit le google-services.json automatiquement, et iOS a besoin d'un appel explicite à Firebase.Core.App.Configure() :

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder()
        .UseMauiApp<App>()
        .UseFirebase(); // extension du plugin

    builder.Services.AddSingleton<IPushNotificationService, PushNotificationService>();
    return builder.Build();
}

Créez ensuite un service qui demande la permission, récupère le jeton et écoute les messages entrants. Ce code fonctionne sur les deux plateformes grâce à l'abstraction CrossFirebaseCloudMessaging.Current :

public class PushNotificationService : IPushNotificationService
{
    public async Task<string?> RegisterAsync()
    {
        var granted = await CrossFirebaseCloudMessaging.Current
            .CheckIfValidAsync();

        if (!granted) return null;

        CrossFirebaseCloudMessaging.Current.TokenChanged += OnTokenChanged;
        CrossFirebaseCloudMessaging.Current.NotificationReceived += OnReceived;
        CrossFirebaseCloudMessaging.Current.NotificationTapped += OnTapped;

        return await CrossFirebaseCloudMessaging.Current.GetTokenAsync();
    }

    private void OnTokenChanged(object? sender, FCMTokenChangedEventArgs e)
        => SendTokenToBackend(e.Token);

    private void OnReceived(object? sender, FCMNotificationReceivedEventArgs e)
    {
        // App au premier plan : afficher un in-app banner
    }

    private void OnTapped(object? sender, FCMNotificationTappedEventArgs e)
    {
        var route = e.Notification.Data.GetValueOrDefault("route");
        if (!string.IsNullOrEmpty(route))
            Shell.Current.GoToAsync(route);
    }
}

Envoyez le jeton à votre backend dès son obtention et à chaque événement TokenChanged. iOS régénère le jeton après réinstallation ou restauration iCloud, et Android le tourne périodiquement pour raisons de sécurité. Un backend qui garde un vieux jeton envoie dans le vide et sature vos quotas FCM.

Comment gérer les notifications en arrière-plan dans .NET MAUI ?

Honnêtement, c'est ici que la plupart des devs se cassent les dents. Le comportement change radicalement selon l'état de l'app. Ce tableau récapitule ce qu'il faut implémenter par état et par plateforme, collez-le au-dessus de votre poste de travail :

État de l'appiOS (APNs)Android (FCM)
Premier planwillPresent délégué, vous choisissez d'afficherOnMessageReceived, pas d'affichage auto
Arrière-planSystème affiche, callback si content-availableSystème affiche si notification, callback si data
Arrêté (killed)Système affiche, payload récupérable au relaunchIdem, via Intent.Extras
Type de payload requisaps.alert ou content-available:1Bloc notification ou data
Limite de taille4 KB (5 KB VoIP)4 KB

Sur iOS, si vous voulez qu'une notification déclenche du code en arrière-plan (pré-téléchargement, mise à jour de badge, sync), envoyez un payload silencieux avec content-available: 1 et sans bloc alert. Attention, depuis iOS 15, Apple applique un throttling agressif. Si vous en envoyez plus de deux ou trois par heure, le système les retarde ou les jette. C'est documenté (discrètement) dans les release notes iOS 15 et confirmé dans les WWDC 2023 à 2025.

Sur Android, la distinction critique, c'est notification message vs data message. Un « notification message » (contenant un objet notification) est géré par le système FCM lui-même quand l'app est en arrière-plan : votre code n'est pas appelé du tout. Un « data message » (contenant seulement data) déclenche toujours OnMessageReceived, ce qui vous laisse le contrôle. Pour du deep linking fiable, envoyez toujours des data messages et construisez la notification vous-même avec NotificationCompat.Builder.

Deep linking depuis une notification vers une page Shell

Le deep linking, c'est là où la plupart des implémentations échouent silencieusement. La règle : passez toujours une route Shell dans le champ data du payload, et déclenchez la navigation après que l'UI soit prête. J'ai galéré sur ce bug pendant deux jours en 2024 : sur une app arrêtée, appeler Shell.Current.GoToAsync depuis OnTapped plante parce que Shell.Current est encore null.

Le pattern fiable : stocker la route en attente, puis rejouer la navigation depuis App.OnStart ou Shell.Loaded. Voir mon guide complet sur la navigation Shell dans MAUI 10 pour la logique de routing sous-jacente et les paramètres de requête typés.

public partial class App : Application
{
    public static string? PendingDeepLink { get; set; }

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

Dans votre OnTapped, ne naviguez donc pas directement. Assignez App.PendingDeepLink = route et laissez OnStart faire le travail. Testez systématiquement les trois scénarios : app au premier plan (navigation immédiate), app en arrière-plan (reprise plus navigation), app killed (démarrage à froid plus navigation). L'échec le plus fréquent, c'est le troisième.

Permissions de notification : iOS 10+ et Android 13+

Sur iOS, vous devez appeler UNUserNotificationCenter.Current.RequestAuthorization avant tout enregistrement APNs. Le paquet Plugin.Firebase le fait pour vous via CheckIfValidAsync(), mais vous devez ajouter la clé NSUserNotificationsUsageDescription dans Info.plist avec un texte explicatif. Apple refuse les apps sans description depuis iOS 15.

Sur Android 13+ (API 33), utilisez Permissions.RequestAsync<Permissions.PostNotifications>() depuis Essentials, ou passez par ActivityCompat.RequestPermissions. Le prompt système n'apparaît qu'une seule fois. Si l'utilisateur refuse, vous devez le rediriger vers Settings avec un Intent : vous ne pouvez pas redemander.

Pensez aussi au badging (nombre rouge sur l'icône). iOS le gère via aps.badge dans le payload. Vous devez maintenir la valeur côté serveur, car le décrémenter côté client demande un appel API. Android n'a pas de badging standard : chaque launcher (Samsung One UI, Pixel Launcher, MIUI) l'implémente différemment, ou pas du tout.

Tester les notifications push en local

Trois outils sauvent la journée. D'abord, la console Firebase permet d'envoyer un push de test à un jeton spécifique en trois clics via Cloud Messaging → New notification → Send test message. Copiez le jeton depuis vos logs, collez, cliquez.

Pour un contrôle fin du payload, utilisez l'outil en ligne de commande httpie avec l'API FCM v1 (l'API legacy est retirée depuis juin 2024) :

http POST https://fcm.googleapis.com/v1/projects/YOUR_PROJECT/messages:send \
  "Authorization:Bearer $(gcloud auth print-access-token)" \
  message:='{"token":"DEVICE_TOKEN","data":{"route":"//detail?id=42"}}'

Pour iOS, testez directement contre APNs (utile pour éliminer Firebase comme suspect) via l'outil apns-tool ou une commande curl HTTP/2 avec votre .p8. C'est un peu plus rugueux, mais irremplaçable pour déboguer un problème de certificat.

Enfin, intégrez ces tests dans votre pipeline CI/CD. Vous pouvez déclencher un push de smoke test après chaque build de préproduction. Couplez ça à votre pipeline GitHub Actions pour MAUI pour valider automatiquement que les jetons se génèrent bien à chaque build. Et si votre architecture d'app suit un pattern MVVM propre, injectez IPushNotificationService via DI comme n'importe quel autre service. Voyez mon guide MVVM avec CommunityToolkit pour la configuration DI recommandée.

Questions fréquentes

.NET MAUI 10 prend-il en charge les notifications push nativement ?

Non. MAUI 10 n'expose aucune API push par défaut. Vous devez brancher FCM (Android) et APNs (iOS) via un plugin comme Plugin.Firebase.CloudMessaging, ou appeler directement les SDK natifs. C'est un choix délibéré de l'équipe .NET : la surface API push est trop divergente entre plateformes pour être abstraite proprement dans le framework.

Quelle est la différence entre FCM et APNs ?

FCM est le service push de Google pour Android (et web, iOS via APNs relay). APNs est le service push d'Apple, obligatoire pour toute notification sur iOS/iPadOS/macOS. FCM peut router vers APNs, c'est ce que fait Plugin.Firebase, mais vous devez configurer votre clé APNs dans Firebase pour que ça fonctionne.

Comment tester les notifications push sans backend ?

Utilisez la console Firebase : Cloud Messaging → New notification → Send test message. Collez un jeton d'appareil récupéré depuis vos logs. Alternative : curl ou httpie contre l'API FCM v1 avec un token OAuth obtenu via gcloud auth print-access-token.

Pourquoi mes notifications iOS n'arrivent-elles pas en production ?

Le coupable numéro un : votre Entitlements.plist a aps-environment à development, mais votre build utilise un profil de provisioning App Store (production). APNs génère alors des jetons non-livrables silencieusement. Passez à production pour toute build de release.

Faut-il migrer depuis Xamarin.Firebase vers Plugin.Firebase pour MAUI ?

Oui. Xamarin.Firebase.iOS/Android n'est plus supporté depuis mai 2024 et ne compile plus proprement sous .NET 10. Plugin.Firebase 3.x cible spécifiquement .NET 8/9/10 et suit les release des SDK Firebase natifs (iOS 11.x, Android BoM 33.x en 2026).

David O'Reilly
À propos de l'auteur David O'Reilly

Native iOS/Android specialist turned MAUI advocate. Writes about the gritty platform details most cross-platform tutorials skip.