اگر تا اینجای سری مقالات .NET MAUI با ما همراه بودهاید، احتمالاً دیگر با MVVM، SQLite، REST API و JWT Authentication دستوپنجه نرم کردهاید. خب، حالا وقت آن است که اپتان را به دنیای واقعی متصل کنیم؛ یعنی پیامرسانی Push. صادقانه بگویم: بدون نوتیفیکیشن، کاربر بعد از چند روز اپ شما را باز نخواهد کرد و نمودار retention مثل سنگ سقوط میکند (این را از تجربهٔ پروژهٔ قبلیام میگویم).
این راهنما در سال ۲۰۲۶ نوشته شده و با آخرین تغییرات .NET MAUI 9، Firebase Cloud Messaging HTTP v1 API و گواهیهای جدید APNs Authentication Key کاملاً سازگار است. تا انتهای مقاله، یک سیستم نوتیفیکیشن سرپا خواهید داشت که:
- روی اندروید با FCM و روی iOS از طریق FCM با APNs کار میکند
- توکن دستگاه را به Backend میفرستد و چرخهحیاتاش را مدیریت میکند
- اعلانها را در سه حالت Foreground، Background و Killed بهدرستی نمایش میدهد
- از Deep Link و Payload سفارشی پشتیبانی میکند
- Permission مدرن (Android 13+ و iOS) را به شکل صحیح میگیرد
چرا Firebase Cloud Messaging؟
FCM رایگان است، مقیاسپذیر است و عملاً استاندارد دوفکتو در صنعت محسوب میشود. مزایای کلیدی FCM در سال ۲۰۲۶:
- Unified API: یک Payload واحد برای اندروید و iOS (FCM در پشتصحنه با APNs گفتگو میکند)
- Topic Messaging: ارسال پیام به میلیونها کاربر بدون نیاز به مدیریت توکن
- Conditional Targeting: ترکیب موضوعات (مثلاً
'news' in topics && 'fa' in topics) - Analytics Integration: یکپارچگی با Firebase Analytics
گزینههای جایگزین مثل Azure Notification Hubs، OneSignal و AWS SNS هم وجود دارند، اما FCM بهخاطر نبود کارمزد و SDK سطح بالاتر، نقطهٔ شروع منطقی است. (شخصاً برای پروژههای B2C تقریباً همیشه با FCM شروع میکنم و فقط در صورت نیاز به Hybrid push روی Azure میروم.)
پیشنیازها
- .NET MAUI 9 و Visual Studio 2026 (یا VS Code با MAUI Workload)
- یک پروژهٔ Firebase در
console.firebase.google.com - برای iOS: Apple Developer Account، Bundle ID و کلید APNs از نوع
.p8 - برای اندروید: حداقل API 21 (Lollipop) — و برای Android 13+ نیاز به runtime permission
گام ۱: ساخت پروژهٔ Firebase
- به Firebase Console بروید و یک پروژهٔ جدید بسازید.
- برای اندروید، اپ جدیدی با Package Name مطابق
ApplicationIdپروژهٔ MAUI خود اضافه کنید (مثلاًcom.yourcompany.mobiletechlead). - فایل
google-services.jsonرا دانلود کنید. - برای iOS، اپ دیگری با Bundle ID اضافه کنید و فایل
GoogleService-Info.plistرا دانلود کنید. - در Project Settings → Cloud Messaging، APNs Auth Key (.p8) را آپلود کنید (Team ID و Key ID لازم میشوند).
یک نکتهٔ کوچک: اگر اشتباهاً Bundle ID را با حروف بزرگ تایپ کنید، بعداً Firebase بهسادگی این را به شما نمیگوید — صرفاً توکن صادر نمیشود و ساعتها وقتتان را میخورد. تجربهٔ شخصی!
گام ۲: نصب پکیجهای مورد نیاز
برای .NET MAUI 9، استفاده از پکیج Plugin.Firebase.CloudMessaging (نگهداریشده توسط Tobias Tobiasen) یا Shiny.Push توصیه میشود. در این مقاله از Plugin.Firebase استفاده میکنیم چون پوشش تقریباً کاملی از سرویسهای Firebase دارد.
dotnet add package Plugin.Firebase.CloudMessaging --version 3.1.0
dotnet add package Plugin.Firebase.Core --version 3.1.0
پلتفرم اندروید: قرار دادن google-services.json
فایل google-services.json را در مسیر Platforms/Android/ قرار دهید و Build Action را روی GoogleServicesJson تنظیم کنید. در Visual Studio معمولاً بهصورت خودکار شناسایی میشود؛ در غیر این صورت دستی به .csproj اضافهاش کنید:
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<GoogleServicesJson Include="Platforms\Android\google-services.json" />
</ItemGroup>
پلتفرم iOS: قرار دادن GoogleService-Info.plist
فایل GoogleService-Info.plist را در Platforms/iOS/ قرار دهید و Build Action را BundleResource کنید:
<ItemGroup Condition="$(TargetFramework.Contains('-ios'))">
<BundleResource Include="Platforms\iOS\GoogleService-Info.plist" />
</ItemGroup>
گام ۳: پیکربندی MauiProgram.cs
حالا در MauiProgram.cs، Firebase را Initialize و Cloud Messaging را Register میکنیم:
using Plugin.Firebase.CloudMessaging;
using Plugin.Firebase.Core;
#if IOS
using Plugin.Firebase.Core.Platforms.iOS;
#elif ANDROID
using Plugin.Firebase.Core.Platforms.Android;
#endif
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.RegisterFirebaseServices()
.ConfigureLifecycleEvents(events =>
{
#if IOS
events.AddiOS(ios => ios.FinishedLaunching((app, launchOptions) =>
{
CrossFirebase.Initialize();
FirebaseCloudMessagingImplementation.Initialize(app, launchOptions);
return false;
}));
#elif ANDROID
events.AddAndroid(android => android.OnCreate((activity, _) =>
CrossFirebase.Initialize(activity)));
#endif
});
builder.Services.AddSingleton(_ => CrossFirebaseCloudMessaging.Current);
builder.Services.AddSingleton<IPushNotificationService, PushNotificationService>();
return builder.Build();
}
private static MauiAppBuilder RegisterFirebaseServices(this MauiAppBuilder builder)
{
// اگر نسخهٔ جدیدتر Plugin.Firebase را دارید، میتوانید از RegisterFirebase خودش استفاده کنید
return builder;
}
}
گام ۴: تنظیمات پلتفرمی
اندروید: AndroidManifest.xml
برای Android 13+ (API 33) باید Permission مربوط به POST_NOTIFICATIONS را در Manifest اعلام کنید:
<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" />
<application android:allowBackup="true" android:icon="@mipmap/appicon">
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/notification_icon" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="default_channel" />
</application>
</manifest>
iOS: Entitlements و Info.plist
یک فایل Entitlements.plist در Platforms/iOS اضافه کنید:
<?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>
</dict>
</plist>
و در Info.plist، Background Modes را برای دریافت Silent Push اضافه کنید:
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>fetch</string>
</array>
گام ۵: درخواست Permission و دریافت توکن
برای اینکه با ساختار MVVM مقالهٔ قبلی هماهنگ باشیم، یک سرویس قابل تزریق برای انتزاع منطق Push میسازیم:
public interface IPushNotificationService
{
Task<bool> RequestPermissionAsync();
Task<string?> GetTokenAsync();
Task SubscribeToTopicAsync(string topic);
Task UnsubscribeFromTopicAsync(string topic);
event EventHandler<NotificationReceivedEventArgs>? NotificationReceived;
event EventHandler<NotificationTappedEventArgs>? NotificationTapped;
}
public class NotificationReceivedEventArgs : EventArgs
{
public string? Title { get; init; }
public string? Body { get; init; }
public IDictionary<string, object> Data { get; init; } = new Dictionary<string, object>();
}
public class NotificationTappedEventArgs : EventArgs
{
public IDictionary<string, object> Data { get; init; } = new Dictionary<string, object>();
}
و حالا پیادهسازی با Plugin.Firebase:
using Plugin.Firebase.CloudMessaging;
using Plugin.Firebase.CloudMessaging.EventArgs;
public class PushNotificationService : IPushNotificationService
{
private readonly IFirebaseCloudMessaging _fcm;
private readonly ILogger<PushNotificationService> _logger;
public event EventHandler<NotificationReceivedEventArgs>? NotificationReceived;
public event EventHandler<NotificationTappedEventArgs>? NotificationTapped;
public PushNotificationService(IFirebaseCloudMessaging fcm, ILogger<PushNotificationService> logger)
{
_fcm = fcm;
_logger = logger;
_fcm.NotificationReceived += OnNotificationReceived;
_fcm.NotificationTapped += OnNotificationTapped;
}
public async Task<bool> RequestPermissionAsync()
{
try
{
var granted = await _fcm.CheckIfValidAsync();
return granted;
}
catch (Exception ex)
{
_logger.LogError(ex, "خطا در درخواست دسترسی نوتیفیکیشن");
return false;
}
}
public async Task<string?> GetTokenAsync()
{
try
{
return await _fcm.GetTokenAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "خطا در دریافت توکن FCM");
return null;
}
}
public Task SubscribeToTopicAsync(string topic) => _fcm.SubscribeToTopicAsync(topic);
public Task UnsubscribeFromTopicAsync(string topic) => _fcm.UnsubscribeFromTopicAsync(topic);
private void OnNotificationReceived(object? sender, FCMNotificationReceivedEventArgs e)
{
NotificationReceived?.Invoke(this, new NotificationReceivedEventArgs
{
Title = e.Notification.Title,
Body = e.Notification.Body,
Data = e.Notification.Data ?? new Dictionary<string, object>()
});
}
private void OnNotificationTapped(object? sender, FCMNotificationTappedEventArgs e)
{
NotificationTapped?.Invoke(this, new NotificationTappedEventArgs
{
Data = e.Notification.Data ?? new Dictionary<string, object>()
});
}
}
گام ۶: ارسال توکن به Backend
توکن FCM یک شناسهٔ بلندمدت اما قابل تغییر است. هر بار که اپ نصب میشود، کاربر دادهها را Clear میکند، یا اپ مدت طولانی غیرفعال میماند، توکن میتواند عوض شود. الگوی درست این است:
- توکن را پس از Login (و دریافت JWT) به API ارسال کنید (با همان Refit از مقالهٔ قبلی)
- تغییرات توکن را با Subscription روی
TokenRefreshedردیابی کنید - هنگام Logout، توکن را در Backend غیرفعال کنید
public interface IDeviceTokenApi
{
[Post("/api/devices")]
Task RegisterAsync([Body] DeviceTokenRequest request);
[Delete("/api/devices/{token}")]
Task UnregisterAsync(string token);
}
public record DeviceTokenRequest(string Token, string Platform, string AppVersion);
public class DeviceTokenSyncService
{
private readonly IPushNotificationService _push;
private readonly IDeviceTokenApi _api;
private readonly IPreferences _prefs;
public DeviceTokenSyncService(IPushNotificationService push, IDeviceTokenApi api, IPreferences prefs)
{
_push = push;
_api = api;
_prefs = prefs;
}
public async Task SyncTokenAsync()
{
var token = await _push.GetTokenAsync();
if (string.IsNullOrEmpty(token)) return;
var lastToken = _prefs.Get<string>("last_fcm_token", string.Empty);
if (token == lastToken) return; // تغییری ایجاد نشده، پس Backend را اذیت نمیکنیم
await _api.RegisterAsync(new DeviceTokenRequest(
token,
DeviceInfo.Platform.ToString(),
AppInfo.VersionString));
_prefs.Set("last_fcm_token", token);
}
}
گام ۷: مدیریت چرخهحیات نوتیفیکیشن
یکی از پیچیدگیهای اصلی Push، مدیریت سه حالت متفاوت اپ است. خیلی از باگهای پروداکشن دقیقاً از همینجا میآیند:
| حالت اپ | اندروید | iOS |
|---|---|---|
| Foreground (باز و فعال) | FCM Callback اجرا میشود؛ نمایش UI بهعهدهٔ شماست | Callback اجرا میشود |
| Background (در حافظه) | سیستم نوتیفیکیشن را نمایش میدهد | سیستم نمایش میدهد |
| Killed (بسته) | پس از Tap، Activity ساخته و Intent ارسال میشود | پس از Tap، launchOptions حاوی Payload است |
نمایش نوتیفیکیشن در Foreground (اندروید)
بهصورت پیشفرض، اندروید در حالت Foreground نوتیفیکیشن را نمایش نمیدهد — و این رفتار خیلیها را گیج میکند. باید خودتان در FirebaseMessagingService یک NotificationCompat بسازید:
[Service(Exported = true)]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
public class MyFirebaseMessagingService : FirebaseMessagingService
{
public override void OnMessageReceived(RemoteMessage message)
{
base.OnMessageReceived(message);
var notification = message.GetNotification();
if (notification == null) return;
var intent = new Intent(this, typeof(MainActivity));
intent.AddFlags(ActivityFlags.ClearTop);
foreach (var kvp in message.Data)
intent.PutExtra(kvp.Key, kvp.Value);
var pendingIntent = PendingIntent.GetActivity(
this, 0, intent, PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Immutable);
var builder = new NotificationCompat.Builder(this, "default_channel")
.SetContentTitle(notification.Title)
.SetContentText(notification.Body)
.SetSmallIcon(Resource.Drawable.notification_icon)
.SetAutoCancel(true)
.SetContentIntent(pendingIntent);
var manager = NotificationManagerCompat.From(this);
manager.Notify(new Random().Next(), builder.Build());
}
}
گام ۸: Deep Linking با Payload سفارشی
بیایید یک سناریوی واقعی را در نظر بگیریم: کاربر روی نوتیفیکیشن «سفارش شما ارسال شد» کلیک میکند و انتظار دارد مستقیم به صفحهٔ جزئیات سفارش برود. در FCM، فیلد data دقیقاً برای همین کار طراحی شده است:
// نمونه Payload از Backend (FCM HTTP v1)
{
"message": {
"token": "DEVICE_FCM_TOKEN",
"notification": {
"title": "سفارش شما ارسال شد",
"body": "سفارش #۱۲۳۴ به محل تحویل رسید"
},
"data": {
"type": "order_update",
"order_id": "1234",
"deep_link": "//orders/1234"
},
"android": {
"priority": "high"
},
"apns": {
"payload": {
"aps": { "sound": "default", "badge": 1 }
}
}
}
}
و در App.xaml.cs (یا یک کلاس NavigationOrchestrator اختصاصی):
public partial class App : Application
{
private readonly IPushNotificationService _push;
public App(IPushNotificationService push)
{
InitializeComponent();
_push = push;
_push.NotificationTapped += OnNotificationTapped;
MainPage = new AppShell();
}
private async void OnNotificationTapped(object? sender, NotificationTappedEventArgs e)
{
if (e.Data.TryGetValue("deep_link", out var link) && link is string route)
{
await Shell.Current.GoToAsync(route);
}
else if (e.Data.TryGetValue("type", out var type) && type?.ToString() == "order_update")
{
var orderId = e.Data["order_id"].ToString();
await Shell.Current.GoToAsync($"//orders/{orderId}");
}
}
}
گام ۹: ارسال نوتیفیکیشن از Backend با FCM HTTP v1
یک نکتهٔ مهم: FCM Legacy API (همان Server Key قدیمی) در سال ۲۰۲۴ Deprecated شد و در ۲۰۲۶ کاملاً حذف شده است. پس حتماً از HTTP v1 با OAuth 2.0 استفاده کنید — اگر هنوز کسی به شما گفت Server Key بفرستی، احتمالاً مستندات قدیمی دیده.
// در پروژهٔ ASP.NET Core Backend
using FirebaseAdmin;
using FirebaseAdmin.Messaging;
using Google.Apis.Auth.OAuth2;
public class FcmSender
{
public FcmSender(IConfiguration config)
{
if (FirebaseApp.DefaultInstance == null)
{
FirebaseApp.Create(new AppOptions
{
Credential = GoogleCredential.FromFile(config["Firebase:ServiceAccountPath"])
});
}
}
public async Task SendOrderUpdateAsync(string token, string orderId)
{
var message = new Message
{
Token = token,
Notification = new Notification
{
Title = "سفارش شما ارسال شد",
Body = $"سفارش #{orderId} به محل تحویل رسید"
},
Data = new Dictionary<string, string>
{
["type"] = "order_update",
["order_id"] = orderId,
["deep_link"] = $"//orders/{orderId}"
},
Android = new AndroidConfig { Priority = Priority.High },
Apns = new ApnsConfig
{
Aps = new Aps { Sound = "default", Badge = 1 }
}
};
var response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
// response = نام پیام در سیستم FCM
}
}
گام ۱۰: تست در Development
برای تست بدون نیاز به Backend واقعی، Firebase Console بخش Messaging را باز کنید، روی New Campaign → Notifications بزنید و توکن دستگاه تست را وارد کنید. اما اگر میخواهید جدیتر تست کنید:
- اندروید: لاگ
adb logcat | grep FirebaseMessagingرا بررسی کنید تا توکن و پیام را زنده ببینید. - iOS: از فایلهای
.apnsدر سیمولاتور Xcode 15+ استفاده کنید (کافی است Drag & Drop کنید روی سیمولاتور). - هر دو پلتفرم: ابزار
postmanیاcurlباaccess_tokenاز Service Account.
اعلانهای محلی (Local Notifications)
برای یادآوریها، تایمرها و پیامهایی که اساساً نیازی به سرور ندارند، از پکیج Plugin.LocalNotification استفاده کنید:
dotnet add package Plugin.LocalNotification --version 11.0.0
using Plugin.LocalNotification;
var notification = new NotificationRequest
{
NotificationId = 100,
Title = "یادآوری ورزش",
Description = "وقتشه که ۳۰ دقیقه پیادهروی کنی!",
Schedule = new NotificationRequestSchedule
{
NotifyTime = DateTime.Now.AddMinutes(30),
RepeatType = NotificationRepeat.Daily
}
};
await LocalNotificationCenter.Current.Show(notification);
چکلیست Production
- ✅ Permission را در یک نقطهٔ منطقی از Onboarding درخواست کنید، نه روی Splash (تجربهٔ کاربری بهتر و opt-in بالاتر)
- ✅ Token Refresh را در Background sync کنید
- ✅ هنگام Logout، توکن را از Backend پاک کنید — وگرنه ممکن است کاربر بعدی روی همان دستگاه، نوتیفیکیشن کاربر قبلی را ببیند!
- ✅ Notification Channelهای جداگانه برای Promotional، Transactional و Alerts تعریف کنید
- ✅ آیکون نوتیفیکیشن باید سفید-تکرنگ باشد (الزام Material Design است، نه پیشنهاد)
- ✅ Payload را همیشه از طریق فیلد
dataبفرستید، نه فقطnotification - ✅ از
collapse_keyبرای جلوگیری از انباشت اعلانهای تکراری استفاده کنید - ✅ Analytics مربوط به Open Rate و Conversion را با Firebase Analytics وصل کنید
- ✅ سیاست Retry برای ارسال توکن به Backend تعریف کنید (همان Polly از مقالهٔ REST API)
اشتباهات رایج که باید از آنها اجتناب کنید
۱. ذخیرهٔ توکن FCM در Local DB بدون TTL
توکن میتواند هر لحظه عوض شود. حتماً یک Listener روی Token Refresh داشته باشید و در صورت تغییر، توکن جدید را به Backend بفرستید. وگرنه روزی میرسد که نمیفهمید چرا کاربر دیگر نوتیفیکیشن نمیگیرد.
۲. عدم تست در حالت Killed
کاربران اغلب اپ را Force Stop میکنند. حتماً سناریوی Killed را تست کنید — مخصوصاً روی اندرویدهای چینی (MIUI، EMUI) که Battery Optimization تهاجمی دارند. باور کنید این بزرگترین منبع تیکتهای پشتیبانی است.
۳. ارسال دادههای حساس در Body
Body نوتیفیکیشن روی Lock Screen دیده میشود. شمارهٔ کارت، رمز یا OTP را هرگز در Body نفرستید. بهجایش از data-only استفاده کنید و در اپ Decrypt کنید.
۴. عدم پشتیبانی از Silent Push
برای آپدیتهای پسزمینه (مثلاً sync دادهها) از Silent Push استفاده کنید: در iOS با content-available: 1 و در اندروید با Priority High و بدون Notification Body.
سؤالات متداول (FAQ)
آیا FCM در ایران بدون فیلترشکن کار میکند؟
دامنهٔ fcm.googleapis.com روی شبکههای موبایل ایران معمولاً قابل دسترسی است، اما در Wi-Fiهای محدود ممکن است مشکل ایجاد شود. برای reliability بیشتر میتوانید از Pushpole یا Notix بهعنوان لایهٔ پروکسی استفاده کنید، یا اگر نیازهای امنیتی بالایی دارید، یک Self-hosted MQTT broker راهاندازی کنید.
تفاوت Notification Message و Data Message در FCM چیست؟
Notification Message توسط FCM SDK خودکار نمایش داده میشود (در Background). Data Message فقط Payload میفرستد و کنترل کامل را به اپ میدهد. توصیه میشود همیشه Data Message + Notification Payload را با هم بفرستید تا هم نمایش پیشفرض داشته باشید و هم بتوانید Custom Logic اجرا کنید.
آیا برای iOS حتماً به Apple Developer Program نیاز است؟
متأسفانه بله. APNs نیاز به Auth Key (.p8) دارد که فقط با حساب Apple Developer (سالی ۹۹ دلار) قابل دریافت است. در Simulator میتوانید با فایلهای .apns تست کنید، اما توکن واقعی APNs فقط روی دستگاه واقعی صادر میشود.
چگونه میتوانم نوتیفیکیشنهای گروهی برای کاربران مختلف بفرستم؟
از قابلیت Topic Messaging FCM استفاده کنید. هر کاربر را با SubscribeToTopicAsync("news") به Topic موردنظر اضافه کنید و در Backend فقط یک پیام به آن Topic بفرستید. FCM آن را به همهٔ مشترکان میرساند، بدون نیاز به ذخیرهٔ توکنها.
چرا نوتیفیکیشن من روی برخی گوشیهای اندرویدی نمایش داده نمیشود؟
گوشیهای Xiaomi، Huawei، Oppo و Vivo سیاست Battery Optimization تهاجمی دارند که FCM Service را Kill میکند. راهحل: کاربر را راهنمایی کنید که اپ را در Auto-start Whitelist و Battery Saver Exception قرار دهد. همچنین priority: high را برای پیامهای مهم تنظیم کنید.
جمعبندی و گام بعدی
خب، در این مقاله یک سیستم Push Notification کامل برای .NET MAUI روی اندروید و iOS با FCM پیاده کردیم. شما یاد گرفتید که چگونه:
- پروژهٔ Firebase را برای دو پلتفرم پیکربندی کنید
- توکن دستگاه را بهصورت ایمن به Backend ارسال کنید
- Permission، چرخهحیات و Deep Linking را مدیریت کنید
- اعلانهای محلی برای سناریوهای آفلاین بسازید
- از Backend با FirebaseAdmin SDK پیام بفرستید
در مقالهٔ بعدی این سری به سراغ Background Tasks در .NET MAUI خواهیم رفت — اینکه چطور با WorkManager در اندروید و BGTaskScheduler در iOS، sync دادهها و کارهای دورهای را پیاده کنیم. تا آن موقع، اگر روی این مقاله سؤالی داشتید یا به Edge Caseی برخوردید، خوشحال میشوم در کامنتها بشنوم.