راهنمای کامل نوتیفیکیشنهای Push در React Native با Expo Notifications، FCM v1 و APNs (۲۰۲۶)
راهنمای عملی نوتیفیکیشن Push در React Native با Expo Notifications، FCM HTTP v1 و APNs. شامل مجوز اندروید ۱۳ و iOS ۱۵، کانالها، Silent Push و کدهای آماده سرور Node.js با expo-server-sdk برای پروژههای ۲۰۲۶.
برای ارسال نوتیفیکیشن Push در React Native با Expo، ابتدا کتابخانه expo-notifications را نصب کنید، از کاربر مجوز بگیرید، یک Expo Push Token دریافت کنید و آن را به سرور خود بفرستید تا از طریق Expo Push Service پیامها را به FCM v1 API (اندروید) و APNs (iOS) هدایت کند. با غیرفعال شدن API قدیمی FCM Legacy در ژوئن ۲۰۲۴، تمام پروژههای React Native باید تا امروز به FCM v1 مهاجرت کرده باشند. توی این راهنما ۲۰۲۶ روش کامل پیادهسازی، احراز هویت با Service Account، مدیریت مجوزها در iOS 15+ و اندروید 13+ و رفع مشکلات رایج را مرحله به مرحله بررسی میکنیم.
API قدیمی FCM Legacy HTTP از ۲۰ ژوئن ۲۰۲۴ بهطور کامل حذف شد و امروز فقط FCM HTTP v1 با احراز هویت OAuth 2.0 و Service Account کار میکند.
کتابخانه expo-notifications نسخه ۰.۳۱+ در SDK 54 با API جدید Async و پشتیبانی کامل از Notification Categories و Rich Media عرضه شد.
در اندروید ۱۳ به بالا مجوز POST_NOTIFICATIONS اجباری است و باید در Runtime درخواست شود؛ در iOS از نسخه ۱۵ Provisional Authorization برای نوتیفیکیشنهای Quiet در دسترس است.
Expo Push Service یک لایه بدون هزینه بالای FCM v1 و APNs است که Token، صف پیام، بازگشت (Receipt) و خطاهای DeviceNotRegistered را برای شما مدیریت میکند.
نوتیفیکیشنهای Silent (Data-only) در iOS با کلید content-available: 1 و در اندروید با نبود بلوک notification در Payload کار میکنند و برای همگامسازی پسزمینه مناسباند.
Expo Go از SDK 53 دیگر Remote Push در اندروید را پشتیبانی نمیکند؛ برای تست واقعی باید از Development Build با EAS Build استفاده کنید.
معماری نوتیفیکیشن Push در React Native
راستش قبل از هر خط کد، باید مسیر یک نوتیفیکیشن Push از سرور شما تا صفحهقفل کاربر را درک کنید. در React Native با Expo، جریان کار شامل چهار طرف است: اپلیکیشن شما، سرور Backend، سرویس واسط (Expo Push Service یا مستقیم FCM/APNs) و در نهایت سرویسهای Push بومی گوگل و اپل. هر کدام نقش متفاوتی دارند و اگر یکی از آنها بهدرستی پیکربندی نشده باشد، پیام هرگز به کاربر نمیرسد.
وقتی اپ روی دستگاه نصب میشود و کاربر مجوز نوتیفیکیشن را میدهد، سیستمعامل یک Device Token منحصربهفرد صادر میکند. در iOS این یک رشته هگز ۶۴ بایتی است که با APNs گفتگو میکند و در اندروید یک FCM Registration Token است. اگر از Expo Push Service استفاده کنید، این توکن بومی به یک Expo Push Token با فرمت ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] ترجمه میشود که مستقل از پلتفرم است و در سمت سرور راحتتر مدیریت میشود. Expo در پشتصحنه پیام شما را به FCM v1 (برای اندروید) یا APNs (برای iOS) هدایت میکند و Receipt وضعیت تحویل را باز میگرداند.
مسیر جایگزین این است که مستقیم به FCM و APNs متصل شوید. این روش کنترل کامل به شما میدهد اما نگهداری دو مسیر جداگانه، مدیریت کلیدهای امنیتی، و برخورد با تفاوتهای Payload بین دو پلتفرم را نیز به دوش میگذارد. برای اکثر پروژههای React Native ۲۰۲۶، لایه Expo Push Service یک انتخاب متعادل است. رایگان، سریع و بدون Lock-in، چون هر زمان که خواستید میتوانید مستقیم به FCM/APNs کوچ کنید. توی آخرین پروژهای که تحویل دادم دقیقاً همین کار را کردم و پشیمان نشدم.
مهاجرت به FCM HTTP v1 و پیکربندی Service Account
در ۲۰ ژوئن ۲۰۲۴ گوگل بهطور رسمی API قدیمی FCM Legacy HTTP و XMPP را حذف کرد. اگر هنوز از Server Key و اندپوینت /fcm/send استفاده میکنید، تمام پیامهای اندروید شما با خطای ۴۰۴ برمیگردند. API جدید HTTP v1 از OAuth 2.0 با Service Account استفاده میکند و ساختار Payload آن نیز فرق دارد.
برای مهاجرت به FCM v1، این مراحل را دنبال کنید:
وارد Firebase Console شوید و به Project Settings ← Service Accounts بروید.
روی Generate new private key کلیک کنید تا فایل JSON دانلود شود. این فایل شامل client_email، private_key و project_id است.
در سرور خود این JSON را در متغیر محیطی GOOGLE_SERVICE_ACCOUNT_JSON ذخیره کنید و هرگز در Git کامیت نکنید.
اگر از EAS Build استفاده میکنید، این فایل را در Expo Dashboard بخش Credentials آپلود کنید تا Expo Push Service بتواند مستقیم به FCM v1 پروژه شما دسترسی پیدا کند.
در سمت iOS، APNs از مدتها قبل به احراز هویت با کلید .p8 (Auth Key) مهاجرت کرده بود. اگر پروژه شما هنوز از گواهی .p12 منقضیشونده استفاده میکند، وقت آن رسیده که در Apple Developer Portal یک Auth Key جدید بسازید و در EAS Credentials ثبت کنید تا سالانه دیگر با نگرانی گواهی روبهرو نباشید.
نصب و پیکربندی expo-notifications در SDK 54
در پروژه Expo خود expo-notifications را با CLI نصب کنید. این کتابخانه در SDK 54 (منتشرشده ژوئن ۲۰۲۶) به نسخه ۰.۳۱ رسیده و شامل بهبودهایی در پرفورمنس ثبت Token و پشتیبانی از Live Activities در iOS 17+ است.
سپس در فایل app.json پلاگین expo-notifications را اضافه کنید و مسیر آیکون کوچک اندروید و رنگ Accent را تنظیم کنید (اندروید بدون این تنظیم آیکون سفید بیفرم نمایش میدهد):
یکی از سوالات پرتکرار «چرا نوتیفیکیشن Push در iOS دریافت نمیشود؟» پاسخش تقریباً همیشه یک چیز است: مجوز درخواست نشده یا کاربر رد کرده است. در اندروید ۱۳ (API 33) به بعد نیز، مجوز POST_NOTIFICATIONS باید در Runtime گرفته شود. اضافه کردن آن به Manifest کافی نیست. کد زیر هر دو پلتفرم را بهدرستی هندل میکند و برای دستگاههای شبیهساز نیز محافظت دارد:
// src/notifications/permissions.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
export async function requestNotificationPermission(): Promise<boolean> {
if (!Device.isDevice) {
console.warn('Push notifications require a physical device.');
return false;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true,
allowProvisional: false, // true = quiet notifications بدون مجوز صریح
},
});
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('Notification permission not granted:', finalStatus);
return false;
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'پیشفرض',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#0ea5e9',
});
}
return true;
}
پارامتر allowProvisional: true در iOS یک قابلیت قدرتمند است که در نسخه ۱۲ اضافه شد. نوتیفیکیشنهای شما بدون درخواست مجوز صریح تحویل میشوند اما بهجای نمایش روی صفحهقفل، مستقیم به Notification Center میروند. این روش برای اپهای خبری یا Update-محور که نمیخواهند در همان بار اول با درخواست مجوز کاربر را بترسانند بسیار مناسب است. اگر بخش عمده Retention شما به Push وابسته است، ابتدا Provisional را فعال کنید و پس از چند تعامل موفق، مجوز کامل را بخواهید.
دریافت Expo Push Token و ذخیره در سرور
پس از گرفتن مجوز، باید Token بگیرید. توجه کنید که در Expo SDK 54 متد getExpoPushTokenAsync به یک آرگومان projectId نیاز دارد که از expo-constants استخراج میشود:
// src/notifications/token.ts
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { requestNotificationPermission } from './permissions';
export async function registerForPushNotifications(): Promise<string | null> {
const granted = await requestNotificationPermission();
if (!granted) return null;
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error('EAS Project ID not found. Run `eas init` first.');
}
const tokenData = await Notifications.getExpoPushTokenAsync({ projectId });
const expoPushToken = tokenData.data;
// ارسال Token به سرور برای ذخیره در دیتابیس
await fetch('https://api.yourbackend.com/push-tokens', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: expoPushToken,
platform: Platform.OS,
deviceId: Constants.deviceId,
}),
});
return expoPushToken;
}
در سمت سرور، توکنها را در جدولی با ستونهای user_id، token، platform، last_active_at ذخیره کنید. هر بار که اپ باز میشود Token را دوباره بگیرید و اگر تغییر کرده باشد بهروزرسانی کنید. این کار ضروری است چون iOS و اندروید به دلایل مختلف (بازنصب اپ، بازیابی از پشتیبان، رفع Token غیرفعال) Token جدید صادر میکنند. برای درک بهتر مدیریت دادههای کاربر در سمت کلاینت میتوانید راهنمای جامع مدیریت State با Zustand و TanStack Query را مطالعه کنید.
ارسال نوتیفیکیشن از سرور Node.js
برای ارسال، از پکیج رسمی expo-server-sdk استفاده کنید که Chunking خودکار (Expo نهایتاً ۱۰۰ توکن در هر درخواست میپذیرد)، بازپخش خطا و ماژول Receipt را برای شما فراهم میکند:
// server/send-push.ts
import { Expo, ExpoPushMessage, ExpoPushTicket } from 'expo-server-sdk';
const expo = new Expo({
accessToken: process.env.EXPO_ACCESS_TOKEN, // اختیاری اما توصیهشده
useFcmV1: true, // پیشفرض در SDK 3.10+ اما صریح بنویسید
});
export async function sendPushToUsers(
tokens: string[],
title: string,
body: string,
data: Record<string, unknown> = {}
) {
const messages: ExpoPushMessage[] = [];
for (const pushToken of tokens) {
if (!Expo.isExpoPushToken(pushToken)) {
console.warn(`Invalid Expo push token: ${pushToken}`);
continue;
}
messages.push({
to: pushToken,
sound: 'default',
title,
body,
data,
priority: 'high',
channelId: 'default',
});
}
const chunks = expo.chunkPushNotifications(messages);
const tickets: ExpoPushTicket[] = [];
for (const chunk of chunks) {
try {
const ticketChunk = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...ticketChunk);
} catch (error) {
console.error('Push chunk failed:', error);
}
}
// ۱۵ دقیقه بعد Receipt را بگیرید تا از تحویل مطمئن شوید
const receiptIds = tickets
.filter((t): t is Extract<ExpoPushTicket, { status: 'ok' }> => t.status === 'ok')
.map(t => t.id);
return receiptIds;
}
بعد از ۱۵ تا ۳۰ دقیقه، از متد getPushNotificationReceiptsAsync استفاده کنید تا وضعیت تحویل واقعی (نه فقط پذیرش صف) را بررسی کنید. اگر Receipt خطای DeviceNotRegistered باز گرداند، Token را از دیتابیس حذف کنید. کاربر اپ را حذف کرده یا نوتیفیکیشنها را غیرفعال کرده است. راستش من یک بار همین را نادیده گرفتم و بعد از دو ماه حساب Expo پروژهام Throttle شد. Tokenهای مرده را جدی بگیرید.
مدیریت نوتیفیکیشن در Foreground و Background
یکی از اشتباهات رایج تازهکارها این است که فرض میکنند نوتیفیکیشن همیشه بهطور خودکار نمایش داده میشود. در واقع، وقتی اپ در Foreground است، iOS و اندروید نوتیفیکیشن Banner را نمایش نمیدهند مگر آنکه صریح فعال کنید. این کار با setNotificationHandler انجام میشود که باید در بالاترین سطح اپ (نه داخل کامپوننت) تنظیم شود:
// app/_layout.tsx (Expo Router)
// همان ابتدای فایل، خارج از هر کامپوننت
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true, // در SDK 54 جایگزین shouldShowAlert
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
برای پاسخ به تعامل کاربر (Tap روی نوتیفیکیشن) از دو Listener استفاده کنید: یکی برای زمانی که نوتیفیکیشن در Foreground رسیده و یکی برای زمانی که کاربر روی آن Tap کرده (Background یا Killed):
وقتی اپ در حالت Killed است و کاربر روی نوتیفیکیشن Tap میکند، اپ باز میشود اما Listener بالا هنوز Mount نشده است. برای این حالت از getLastNotificationResponseAsync در همان ابتدای Root Layout استفاده کنید تا Deep Link را از دست ندهید. اگر با ناوبری آشنا نیستید، راهنمای Deep Linking در React Native با Expo Router نکات عمیقتری در این باره دارد.
ساخت کانالهای نوتیفیکیشن اندروید
از اندروید ۸ (Oreo) به بعد، هر نوتیفیکیشن باید به یک کانال (Notification Channel) تعلق داشته باشد. کانالها به کاربر اجازه میدهند دستهبندی نوتیفیکیشنهای شما را جداگانه غیرفعال کند. مثلاً یک اپ خبری میتواند سه کانال داشته باشد: «اخبار فوری»، «بهروزرسانی روزانه» و «پیام مستقیم». کاربر میتواند فقط کانال روزانه را ساکت کند بدون آنکه اخبار فوری را از دست بدهد.
در سمت سرور، هنگام ارسال Push به Expo، فیلد channelId را برابر با 'urgent-news' یا هر کانالی که پیام به آن تعلق دارد قرار دهید. اگر مستقیم به FCM v1 میفرستید، این مقدار در بلوک android.notification.channel_id قرار میگیرد. توجه کنید: پس از ساخت کانال، ویژگیهای آن قابل تغییر نیستند مگر با حذف کانال. بنابراین از ابتدا نامها و اهمیتها را حسابشده انتخاب کنید.
نوتیفیکیشنهای Rich و Notification Categories
نوتیفیکیشن ساده متن و آیکون است، اما در ۲۰۲۶ کاربران انتظار Rich Notifications دارند: تصویر بزرگ، دکمههای تعامل (Reply، Like، Snooze) و در iOS حتی Live Activities. برای دکمههای تعامل از Notification Categories استفاده کنید که در iOS Actions نامیده میشود:
// نوتیفیکیشن با دکمههای Reply و Mark as Read
await Notifications.setNotificationCategoryAsync('message', [
{
identifier: 'reply',
buttonTitle: 'پاسخ',
textInput: { submitButtonTitle: 'ارسال', placeholder: 'پیام...' },
},
{
identifier: 'mark-read',
buttonTitle: 'خوانده شد',
options: { opensAppToForeground: false },
},
]);
// سمت سرور، Payload نوتیفیکیشن با دسته و تصویر
{
to: expoPushToken,
title: 'پیام جدید از سارا',
body: 'سلام! میتونی امشب زنگ بزنی؟',
categoryIdentifier: 'message', // iOS
data: { conversationId: 'conv_123' },
richContent: { image: 'https://cdn.yoursite.com/avatar.jpg' },
channelId: 'direct-message',
}
پاسخ کاربر به این دکمهها در Listener معمولی نوتیفیکیشن قابل دریافت است. فیلد response.actionIdentifier شامل نام دکمه ('reply') و response.userText شامل متن تایپشده کاربر است. این ویژگی برای اپهای چت، Todo و اپهای خبری که Like سریع میخواهند بسیار قدرتمند است. کاربر بدون باز کردن اپ میتواند تعامل کند و شما آمار Engagement بالاتری خواهید داشت. توی یک پروژه پیامرسان که پارسال روی آن کار میکردم، فقط اضافه کردن دکمه Reply نرخ پاسخگویی را حدود ۴۰٪ بالا برد.
نوتیفیکیشنهای Silent و همگامسازی پسزمینه
نوتیفیکیشن Silent (یا Data-only Push) بدون نمایش هیچ چیزی به کاربر، اپ را در پسزمینه بیدار میکند تا کاری انجام دهد. مثل دانلود پیامهای جدید، بهروزرسانی کش، یا Sync آفلاین. این قابلیت بهویژه برای اپهای Offline-First که در راهنمای ساخت اپ Offline-First با Expo SQLite شرح داده شد، حیاتی است.
// سمت سرور، Payload Silent برای iOS و اندروید
{
to: expoPushToken,
data: {
type: 'sync',
syncId: 'sync_456',
},
_contentAvailable: true, // iOS: content-available: 1
// در اندروید: نبود فیلد notification یعنی Data-only
}
// سمت اپ، Background Task
import * as TaskManager from 'expo-task-manager';
import * as Notifications from 'expo-notifications';
const BACKGROUND_TASK = 'background-notification-task';
TaskManager.defineTask(BACKGROUND_TASK, async ({ data, error }) => {
if (error) { console.error(error); return; }
const notification = data as Notifications.Notification;
if (notification.request.content.data.type === 'sync') {
await syncLocalDatabase(); // منطق Sync شما
}
});
Notifications.registerTaskAsync(BACKGROUND_TASK);
دیباگ و رفع خطاهای رایج
وقتی نوتیفیکیشن نمیرسد، مرحله به مرحله بررسی کنید. اولین ابزار Expo Push Notifications Tool در expo.dev/notifications است. Token را پیست کنید، یک پیام تست بفرستید و پاسخ سرور را بلافاصله ببینید. اگر پیام Ticket برمیگرداند اما Receipt خطا میدهد، مشکل در پیکربندی FCM/APNs است نه در اپ شما.
پرتکرارترین خطاها و راهحلها:
InvalidCredentials (اندروید): فایل Service Account در EAS Credentials آپلود نشده یا برای پروژه اشتباهی صادر شده. فایل جدید از Firebase Console بگیرید و مطمئن شوید Package Name در google-services.json با android.package در app.json یکی است.
MessageRateExceeded: شما بیش از ۶۰۰ پیام در ثانیه به یک Token خاص میفرستید. Rate Limiting سمت خودتان اضافه کنید و از Queue استفاده کنید.
Token در iOS بازمیگردد ولی پیام نمیرسد: در Xcode ← Signing & Capabilities مطمئن شوید Push Notifications و Background Modes ← Remote Notifications فعال هستند. اگر EAS Build دارید، در Apple Developer Portal کلید APNs Auth Key معتبر ثبت کرده باشید.
در اندروید ۱۳+ درخواست مجوز نمایش داده نمیشود: اپ خود را حذف و دوباره نصب کنید. یک بار رد کردن، سیستم را برای همیشه Cache میکند مگر با Reset تنظیمات اپ.
نوتیفیکیشن در Foreground نمایش داده نمیشود:setNotificationHandler را در فایل Root با shouldShowBanner: true تنظیم کنید.
برای Log کامل، در سمت اپ از Notifications.addNotificationsDroppedListener برای شنیدن نوتیفیکیشنهای حذفشده استفاده کنید و در سمت سرور، تمام Ticket IDها و Receiptها را ذخیره کنید تا هر پیام Fail شده قابل ردیابی باشد. ابزار Expo Push Notifications FAQ نیز فهرست کامل کدهای خطا و دلایل آنها را دارد. برای شناسایی سریعتر مشکلات مربوط به Service Account، مستندات Google Cloud IAM درباره Service Account را هم مطالعه کنید.
سوالات متداول
تفاوت Expo Push Token و FCM Token چیست؟
Expo Push Token یک شناسه انتزاعی با فرمت ExponentPushToken[...] است که مستقل از پلتفرم کار میکند و Expo Push Service آن را در پشتصحنه به FCM Token (اندروید) یا APNs Token (iOS) ترجمه میکند. FCM Token مستقیم Native است و برای ارسال بدون Expo کاربرد دارد. برای اکثر پروژهها Expo Push Token سادهتر، رایگان و انعطافپذیرتر است.
آیا Expo Notifications در Expo Go کار میکند؟
خیر. از SDK 53 (منتشرشده اردیبهشت ۲۰۲۵) قابلیت Remote Push در Expo Go برای اندروید حذف شده و در iOS نیز بهطور محدود کار میکند. برای تست کامل باید یک Development Build با eas build --profile development بسازید که تمام قابلیتهای Native را شامل میشود.
چرا نوتیفیکیشن Push در iOS دریافت نمیشود؟
سه دلیل اصلی وجود دارد: (۱) کاربر مجوز نداده، با Notifications.getPermissionsAsync چک کنید، (۲) در Xcode Push Notifications Capability فعال نیست، یا (۳) APNs Auth Key در EAS Credentials منقضی یا غلط است. همچنین در Simulator نوتیفیکیشنهای Remote کار نمیکنند، از دستگاه فیزیکی استفاده کنید.
چگونه یک کانال نوتیفیکیشن در اندروید بسازیم؟
از متد Notifications.setNotificationChannelAsync('channel-id', { name, importance, sound, vibrationPattern }) استفاده کنید. کانال باید قبل از اولین ارسال پیام ساخته شود، معمولاً در Root Layout هنگام Boot اپ. پس از ساخت، تنظیمات کانال قابل تغییر نیستند مگر با حذف و ساخت دوباره.
آیا FCM Legacy API هنوز کار میکند؟
خیر. گوگل در ۲۰ ژوئن ۲۰۲۴ اندپوینت /fcm/send و Server Key قدیمی را کاملاً حذف کرد. تمام ارسالها باید از FCM HTTP v1 با OAuth 2.0 و Service Account استفاده کنند. اگر از Expo Push Service (نسخه SDK ۳.۱۰+) استفاده میکنید، این مهاجرت بهطور خودکار انجام شده است.
Silent Notification برای همگامسازی پسزمینه امن است؟
Silent Notification برای شروع یک Sync سبک مناسب است اما نباید تنها مکانیزم شما باشد. iOS نهایتاً چند بار در ساعت اجازه اجرا میدهد و اگر Background App Refresh غیرفعال باشد، هرگز اجرا نمیشود. آن را با BackgroundTasks (iOS) و WorkManager (اندروید) ترکیب کنید و همیشه یک Sync Manual در Launch اپ داشته باشید.