Главная / Статьи

Уведомления в React Native: разрешения, каналы и жизненный цикл FCM

Узнайте, как разрешения, каналы Android, токены FCM и обработчики состояний переднего плана, фона и выхода взаимодействуют друг с другом в системе уведомлений React Native с использованием Notifee.

Издатель: REACTAPP.TOP19 мин чтения4332 слов

Источники: React Native Notifications & Permissions: A Complete Guide for Android and iOS

Для пользователя уведомление — это одна строка текста, например «Ваш заказ отправлен». Для приложения, которое его отправило, эта строка зависит от разрешений во время работы приложения, каналов уведомлений Android, токенов устройства, сервиса Firebase Cloud Messaging (FCM), фоновых обработчиков, а также от различий между платформами Android и iOS. Ошибка на любом из этих уровней приводит к тому, что уведомления так и не поступают или открывается неверный экран. В этом руководстве рассматриваются все эти аспекты с использованием Notifee и React Native Firebase, чтобы вы могли создать систему уведомлений, которая будет работать предсказуемо в любом состоянии приложения.

Локальные уведомления и уведомления через пуш

Локальные уведомления

Локальное уведомление создается на устройстве самим приложением без участия сервера. Типичным примером является напоминание о встрече:

Reminder
Your meeting starts in 10 minutes.

Приложение может отображать уведомления или планировать их отправку в офлайн-режиме. Notifee предоставляет один API для React Native, который работает через встроенные системы уведомлений обеих платформ.

Уведомления типа push

Процесс отправки уведомления типа push начинается на сервере. Ваш бэкенд передает сообщение в FCM, FCM доставляет его на устройство, после чего приложение React Native реагирует на это сообщение:

Your Backend
     │
     ▼
Firebase Cloud Messaging
     │
     ▼
Android / iOS Device
     │
     ▼
React Native Application

FCM различает уведомления (которые система может отобразить для пользователя) и сообщения с данными (арбитрарные данные в формате ключ-значение для вашего кода); кроме того, он может адресовать как отдельное устройство, так и группу устройств с помощью тем.

Разрешения являются частью состояния приложения

Установка приложения больше не подразумевает возможности отображения уведомлений. Начиная с Android 13, POST_NOTIFICATIONS является разрешением во время выполнения, и приложения, предназначенные для Android 13 и новее версий, должны запрашивать его перед отображением любого контента. iOS всегда требовал явного разрешения пользователя. Рассматривайте это разрешение как состояние приложения, которое может меняться, а не как однократный шаг настройки.

Разумный алгоритм включает проверку статуса, объяснение преимуществ, а затем запрос разрешения; любой из ответов позволяет приложению продолжить работу:

Application starts
       │
       ▼
Check notification permission
       │
       ├── Granted ───────► Continue
       │
       └── Not granted
                │
                ▼
        Explain the benefit
                │
                ▼
        Request permission
                │
          ┌─────┴─────┐
          ▼           ▼
       Granted      Denied
          │           │
          ▼           ▼
     Notifications   Continue

Установка библиотек

Notifee отвечает за отображение уведомлений:

npm install @notifee/react-native

React Native Firebase предоставляет основной модуль приложения и модуль обмена сообщениями для доставки уведомлений:

npm install @react-native-firebase/app
npm install @react-native-firebase/messaging

На iOS после этого необходимо установить нативные зависимости CocoaPods:

cd ios
pod install
cd ..

Нативная настройка различается в зависимости от версии React Native и настроек проекта, кроме того, для отправки уведомлений на iOS требуются учетные данные APNs в Firebase и поддержка функции push в Xcode. Следуйте актуальной документации каждой библиотеки.

Отображение первого нативного уведомления

Цель первого примера — создание уведомления с таким заголовком и текстом:

New Message
You have received a new message.

Начните с импорта библиотеки Notifee вместе с энумерацией уровней важности, используемой для каналов на Android:

import notifee, {
  AndroidImportance,
} from '@notifee/react-native';

Приведенная ниже функция выполняет три действия по порядку: она запрашивает разрешение, создает (или повторно использует) канал на Android с именем default, а затем отображает уведомление на этом канале. Параметр pressAction с идентификатором default указывает Android на необходимость открытия приложения при нажатии на уведомление; без него нажатие ничего не вызывает.

export async function displayNotification() {
  // Request permission
  await notifee.requestPermission();  // Create Android notification channel
  const channelId = await notifee.createChannel({
    id: 'default',
    name: 'Default Notifications',
    importance: AndroidImportance.DEFAULT,
  });  // Display notification
  await notifee.displayNotification({
    title: 'New Message',
    body: 'You have received a new message.',    android: {
      channelId,
      pressAction: {
        id: 'default',
      },
    },
  });
}

Повторное вызовы createChannel безопасны; существующий канал просто возвращает свой идентификатор. Каналы обязательны в Android 8.0 (API 26) и новее версиях. Вызовите функцию с помощью кнопки:

<Button
  title="Show Notification"
  onPress={displayNotification}
/>

Запрос разрешения Android 13

Полное название этого разрешения в Android:

android.permission.POST_NOTIFICATIONS

React Native предоставляет его в виде константы в объекте PermissionsAndroid:

PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS

Это разрешение также необходимо указать с помощью элемента uses-permission в файле AndroidManifest.xml, иначе запрос будет незаметно отклонен. Начните с импортов:

import {
  PermissionsAndroid,
  Platform,
} from 'react-native';

Вспомогательная функция сразу возвращает true на iOS и в версиях Android ниже 13 (уровень API 33), где уведомления разрешены по умолчанию. В остальных случаях она отображает системное диалоговое окно и сообщает, был ли результат GRANTED:

export async function requestAndroidNotificationPermission() {
  if (Platform.OS !== 'android') {
    return true;
  }  if (Platform.Version < 33) {
    return true;
  }  const result = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
  );  return result === PermissionsAndroid.RESULTS.GRANTED;
}

Вызовите его с экрана:

const granted =
  await requestAndroidNotificationPermission();

и примените разветвление в зависимости от результата:

if (granted) {
  console.log('Notifications allowed');
} else {
  console.log('Notifications denied');
}

Android может вернуть три значимых результата:

GRANTED
DENIED
NEVER_ASK_AGAIN

NEVER_ASK_AGAIN требует особого внимания. После нескольких отказов система перестает показывать диалог, и только настройки системы могут изменить ответ.

Запрос в подходящий момент

Показ системного запроса сразу на первом экране — это распространенная и дорогостоящая ошибка. Новый пользователь, который сразу увидит

"Allow MyApp to send notifications?"

не имеет контекста и, скорее всего, откажется. Сначала объясните ситуацию, например, на экране приветствия:

Welcome to MyApp

за которым следует карточка с описанием назначения уведомлений и кнопка:

Enable notifications to receive:
✓ New messages
✓ Order updates
✓ Important alerts[Enable Notifications]

Только нажатие на эту кнопку запускает диалог операционной системы, так что запрос отвечает на выбор пользователя, а не прерывает его действия.

Разрешение в iOS с Notifee

В iOS пользователь должен явно разрешить доставку уведомлений. Notifee сообщает о результате через AuthorizationStatus:

import notifee, {
  AuthorizationStatus,
} from '@notifee/react-native';

Приведённый ниже помощник считает два состояния успешными. AUTHORIZED означает полные права. PROVISIONAL — режим тишины в iOS, при котором уведомления доставляются непосредственно в Центр уведомлений без прерывания пользователя, а тот может позже оставить их или отключить. Все остальные случаи считаются отказом:

export async function requestIOSNotificationPermission() {
  const settings = await notifee.requestPermission();  if (
    settings.authorizationStatus ===
    AuthorizationStatus.AUTHORIZED
  ) {
    console.log('Notifications authorized');
    return true;
  }  if (
    settings.authorizationStatus ===
    AuthorizationStatus.PROVISIONAL
  ) {
    console.log('Notifications provisionally authorized');
    return true;
  }  console.log('Notifications denied');  return false;
}

Запрос в iOS может включать ряд опций:

Alerts
Sounds
Badges
CarPlay
Announcements
Provisional notifications

Прошу только то, что вам действительно нужно; например, временное разрешение полностью исключает появление запроса.

Одна функция разрешения для всего приложения

Вместо того чтобы распределять проверки по разным экранам, используйте один модуль обслуживания:

src/
 ├── services/
 │    └── notificationService.ts
 ├── screens/
 ├── components/
 └── App.tsx

Файл службы:

// notificationService.ts

Сочетает пути для обеих платформ. На Android выполняется проверка версии и запрос во время выполнения приложения; на iOS используется Notifee, принимающий как авторизованный, так и временный статус:

import {
  Platform,
  PermissionsAndroid,
} from 'react-native';import notifee, {
  AuthorizationStatus,
} from '@notifee/react-native';export async function requestNotificationPermission() {
  if (Platform.OS === 'android') {
    if (Platform.Version < 33) {
      return true;
    }    const result = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
    );    return result === PermissionsAndroid.RESULTS.GRANTED;
  }  const settings = await notifee.requestPermission();  return (
    settings.authorizationStatus ===
      AuthorizationStatus.AUTHORIZED ||
    settings.authorizationStatus ===
      AuthorizationStatus.PROVISIONAL
  );
}

Затем экраны вызывают одну функцию:

const enabled = await requestNotificationPermission();

и действуют на основе полученного результата:

if (enabled) {
  console.log('Ready for notifications');
}

В новых версиях Notifee также можно запросить POST_NOTIFICATIONS на Android 13 с помощью requestPermission(); ознакомьтесь с текущей документацией Notifee, если хотите использовать один запрос для обеих платформ.

Каналы уведомлений Android

В Android 8.0 были введены каналы, позволяющие пользователям независимо управлять категориями уведомлений. Приложение может предоставить несколько таких каналов:

MyApp
│
├── Messages
├── Orders
├── Promotions
└── Security

У каждого канала есть своя степень важности и звук. Обычно сообщения должны иметь высокую степень важности, чтобы они отображались:

const messageChannel = await notifee.createChannel({
  id: 'messages',
  name: 'Messages',
  importance: AndroidImportance.HIGH,
});

При обновлении заказов можно использовать стандартный уровень:

const orderChannel = await notifee.createChannel({
  id: 'orders',
  name: 'Orders',
  importance: AndroidImportance.DEFAULT,
});

Затем в уведомлении указывается название канала, к которому оно относится. Вызов показан в двух частях: сначала заголовок, затем тело:

await notifee.displayNotification({
  title: 'New Message',
  body: 'You received a new message.',

а затем параметры Android, связанные с идентификатором канала, возвращаемым функцией createChannel:

  android: {
    channelId: messageChannel,
  },
});

Каналы предназначены для пользователей, а не являются лишь настройками разработчика. Пользователи могут отключить звук или заблокировать канал в системных настройках, и после создания канала приложение больше не может программно изменять его приоритет; это решение принимает пользователь. Выбирайте тщательно или используйте новый идентификатор канала позже.

Эффективная структура в производственных условиях разделяет каналы по их назначению:

notifications
│
├── messages
├── orders
├── payments
├── security
└── marketing

Отдельный маркетинговый канал позволяет пользователям отключать уведомления о рекламных акциях, не теряя при этом уведомлений о безопасности.

Как вписывается FCM

Для режима push FCM находится между вашим бэкендом и каждым установленным приложением:

┌─────────────────┐
             │   Your Backend  │
             └────────┬────────┘
                      │
                      ▼
             ┌─────────────────┐
             │      FCM        │
             └────────┬────────┘
                      │
              ┌───────┴───────┐
              ▼               ▼
        ┌──────────┐     ┌──────────┐
        │ Android  │     │   iOS    │
        └──────────┘     └──────────┘

Получение токена устройства

Каждая установка приложения может получить токен регистрации FCM, и ваш бэкенд нуждается в этом токене для обращения к конкретному устройству. Импортируйте модуль обмена сообщениями:

import messaging from '@react-native-firebase/messaging';

и запросите токен:

export async function getFCMToken() {
  const token = await messaging().getToken();  console.log('FCM Token:', token);  return token;
}

В реальном приложении токен отправляется на ваш сервер сразу после его получения:

const token = await messaging().getToken();
await fetch('https://api.example.com/device-token', {
  method: 'POST',  headers: {
    'Content-Type': 'application/json',
  },  body: JSON.stringify({
    token,
  }),
});

Затем бэкенд хранит токен в связи с авторизованным пользователем и устройством:

User
  │
  ├── userId
  ├── deviceId
  └── fcmToken

Обновление токенов

Токены могут измениться, например после переустановки или очистки данных приложения. Подпишитесь на события обновления:

import messaging from '@react-native-firebase/messaging';

Слушатель передает каждый новый токен, а функция возвращает обратный вызов для отписки:

export function subscribeToTokenRefresh() {
  return messaging().onTokenRefresh(async token => {
    console.log('New FCM token:', token);    await sendTokenToBackend(token);
  });
}

Минимальный помощник для загрузки, показанный снова в двух частях, выполняет JSON-запрос POST:

async function sendTokenToBackend(token: string) {
  await fetch('https://api.example.com/device-token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },    body: JSON.stringify({
      token,
    }),
  });
}

Собственные рекомендации Firebase предусматривают обработку изменений токенов и отправку новых значений на ваш сервер приложения каждый раз, когда они меняются. Со стороны сервера также необходимо удалять токены, которые FCM считает недействительными, чтобы не накапливались устаревшие устройства.

Сообщения на первом плане не отображаются автоматически

Самый запутанный случай — это приход сообщения во время работы приложения:

User
  │
  ▼
React Native App
  │
  ▼
FCM message

Прослушивайте сообщения на первом плане:

import messaging from '@react-native-firebase/messaging';
const unsubscribe = messaging().onMessage(
  async remoteMessage => {
    console.log('Foreground message:', remoteMessage);
  },
);

Однако получение сообщения и отображение системного уведомления — это разные шаги. Когда приложение находится на первом плане, FCM передает сообщение в ваш код, но само по себе ничего не отображает. Чтобы что-то показать, передайте содержимое в Notifee:

import notifee from '@notifee/react-native';

Приведённый ниже обработчик убеждается в наличии канала, затем отображает уведомление; при отсутствии блока notification в сообщении (например, при сообщении с только данными) используется стандартный текст:

messaging().onMessage(async remoteMessage => {
  const channelId = await notifee.createChannel({
    id: 'messages',
    name: 'Messages',
  });  await notifee.displayNotification({
    title:
      remoteMessage.notification?.title ??
      'New Message',    body:
      remoteMessage.notification?.body ??
      'You have a new message.',    android: {
      channelId,
      pressAction: {
        id: 'default',
      },
    },
  });
});

Вы контролируете отображение уведомлений, включая возможность его пропуска, если соответствующий экран уже виден.

Уведомления на фоне и при завершении работы

Когда приложение находится на фоне, его поведение меняется. В Android уведомления FCM направляются непосредственно в системную панель без вызова вашего обработчика на переднем плане, а сообщения с одновременным содержанием уведомления и данных подчиняются своим правилам работы на фоне. В iOS уведомления с только данными обрабатываются по принципу «лучшего усилия» и могут быть задержаны или ограничены системой.

React Native Firebase предоставляет специальный обработчик для уведомлений на фоне:

import messaging from '@react-native-firebase/messaging';
messaging().setBackgroundMessageHandler(
  async remoteMessage => {
    console.log(
      'Background message:',
      remoteMessage,
    );
  },
);

Частой ошибкой является регистрация этого обработчика внутри компонента React, как показано в следующем фрагменте:

function App() {
  messaging().setBackgroundMessageHandler(...);
  return <Navigation />;
}

При пробуждении приложения из фона может не загружаться никакой компонент, поэтому обработчик вообще не существует. Регистрируйте его в точке входа:

// index.js
import messaging from '@react-native-firebase/messaging';messaging().setBackgroundMessageHandler(
  async remoteMessage => {
    console.log(
      'Background message:',
      remoteMessage,
    );
  },
);

И только затем регистрируйте корневой компонент:

AppRegistry.registerComponent(
  appName,
  () => App,
);

Реакция на нажатия уведомлений

Нажатие обычно должно переносить пользователя в определённое место:

Notification
     │
     ▼
User taps
     │
     ▼
Application opens
     │
     ▼
Order Details

Предположим, что сообщение содержит идентификатор заказа в своих данных:

{
  "orderId": "12345"
}

Для приложений, возобновлённых из фона, функция onNotificationOpenedApp срабатывает при нажатии пользователя на уведомление. Колбэк считывает идентификатор из remoteMessage.data и может оттуда осуществить переход:

messaging().onNotificationOpenedApp(
  remoteMessage => {
    const orderId =
      remoteMessage.data?.orderId;
    if (orderId) {
      // Navigate to order details
      console.log(
        'Open order:',
        orderId,
      );
    }
  },
);

Когда приложение полностью закрыто

Если приложение было завершено, последовательность действий выглядит следующим образом:

App completely closed
        │
        ▼
User receives notification
        │
        ▼
User taps notification
        │
        ▼
App launches

В этом случае метод onNotificationOpenedApp не срабатывает. Вместо этого при запуске необходимо один раз запросить уведомление, которое запустило приложение:

const remoteMessage =
  await messaging().getInitialNotification();
if (remoteMessage) {
  const orderId =
    remoteMessage.data?.orderId;  console.log(
    'App opened from notification:',
    orderId,
  );
}

Оба метода обрабатывают только те уведомления, которые отображает FCM. Нажатия на уведомления, которые вы сами отобразили с помощью Notifee, такие как уведомления на переднем плане, поступают через собственные API событий Notifee (onForegroundEvent, onBackgroundEvent и getInitialNotification). Обработка только одного типа уведомлений — распространенная причина ошибок вида «нажатие ничего не делает».

Полезные данные для навигации

Уведомление, которое просто говорит

New Order

, не сообщает приложению, куда должен перейти пользователь. Необходимо включить структурированные данные рядом с видимым текстом:

{
  "notification": {
    "title": "New Order",
    "body": "Order #12345 has been confirmed"
  },
  "data": {
    "type": "order",
    "orderId": "12345"
  }
}

Функция маршрутизатора может затем соотнести type с экраном:

function handleNotification(data: any) {
  if (data.type === 'order') {
    navigation.navigate(
      'OrderDetails',
      {
        orderId: data.orderId,
      },
    );
  }
}

Поскольку обработчики уведомлений часто выполняются вне компонентов, navigation в таких помощниках обычно представляет собой ссылку на навигацию, созданную в корне приложения. Подробнее о маршрутизации нажатий через глубокие ссылки см. маршрутизация нажатий на уведомления через глубокие ссылки в React Native.

Обнаружение изменений разрешений после установки

Разрешения могут меняться со временем. Пользователь может разрешить уведомления в понедельник:

Monday:
Notifications = ON

и отключить их в настройках системы в среду:

Wednesday:
User opens Android SettingsNotifications = OFF

Приложение должно это заметить. Notifee считывает текущие настройки без запроса разрешения пользователя:

import notifee, {
  AuthorizationStatus,
} from '@notifee/react-native';
export async function checkNotificationPermission() {
  const settings =
    await notifee.getNotificationSettings();  if (
    settings.authorizationStatus ===
    AuthorizationStatus.AUTHORIZED
  ) {
    return true;
  }  return false;
}

Обратите внимание, что эта проверка возвращает false для временной авторизации; укажите PROVISIONAL, если ваше приложение считает её активной. Перезапустите проверку, когда приложение вернётся на передний план.

Когда пользователь отвечает «нет»

Не повторяйте запрос. Вместо этого покажите чёткое состояние, например баннер:

Notifications are disabled

с действием, ведущим в настройки:

Enable notifications from Settings
to receive important updates.[Open Settings]

Notifee предоставляет API, которые позволяют напрямую открыть настройки уведомлений приложения. Принципы, лежащие в основе всего этого, можно описать на одной карточке:

Don't force
Don't spam
Explain the value
Give the user control

Централизация настройки уведомлений

В производственном коде храните всё, что связано с уведомлениями, в нескольких чётко определённых модулях:

src/
│
├── services/
│   ├── notificationService.ts
│   └── firebaseMessaging.ts
│
├── navigation/
│   └── notificationNavigation.ts
│
├── screens/
│
└── App.tsx

Основная служба:

// notificationService.ts

Предоставляется инициализатор, который запрашивает разрешение пользователя; если пользователь откажет, процесс останавливается, в противном случае создается стандартный канал:

import notifee, {
  AndroidImportance,
  AuthorizationStatus,
} from '@notifee/react-native';export async function initializeNotifications() {
  const settings =
    await notifee.requestPermission();  const authorized =
    settings.authorizationStatus ===
      AuthorizationStatus.AUTHORIZED ||
    settings.authorizationStatus ===
      AuthorizationStatus.PROVISIONAL;  if (!authorized) {
    return false;
  }  await notifee.createChannel({
    id: 'default',
    name: 'Default Notifications',
    importance: AndroidImportance.DEFAULT,
  });  return true;
}

Вызов этого инициализатора из компонента происходит очень просто:

useEffect(() => {
  initializeNotifications();
}, []);

Внимание: на первом экране запрос появляется сразу, что приводит к проблемам с таймингом, описанным ранее. Лучше вызывать его с помощью кнопки с опцией подключения.

Часть сервера

Типичная модель данных хранит пользователей и их устройства в отдельных таблицах, поскольку один пользователь может иметь несколько телефонов или планшетов:

users
│
├── id
├── name
└── email
user_devices
│
├── id
├── user_id
├── device_id
├── fcm_token
├── platform
├── created_at
└── updated_at

Запись о регистрации устройства может выглядеть так:

{
  "userId": 101,
  "deviceId": "device-abc",
  "fcmToken": "xxxxx",
  "platform": "android"
}

При возникновении бизнес-события сервер находит токены пользователя и отправляет сообщение через FCM:

Order Created
      │
      ▼
Backend
      │
      ▼
Find user's FCM token
      │
      ▼
Firebase Cloud Messaging
      │
      ▼
User's phone

FCM ожидает, что сообщения будут отправляться из надежной среды, такой как ваш сервер приложений или Cloud Functions, а не из клиента, поскольку для отправки требуются учетные данные сервера.

Полезная нагрузка, масштабируемая с развитием приложения

Проектируйте каждую полезную нагрузку так, чтобы приложение понимало событие, а не просто текст. Подтверждение оплаты может содержать:

{
  "notification": {
    "title": "Payment Successful",
    "body": "Your payment of ₹999 was successful."
  },
  "data": {
    "type": "payment",
    "paymentId": "PAY12345",
    "screen": "PaymentDetails"
  }
}

Один единственный оператор switch на основе значения data.type обрабатывает все типы уведомлений:

switch (data.type) {
  case 'payment':
    navigation.navigate(
      'PaymentDetails',
      {
        paymentId: data.paymentId,
      },
    );
    break;
  case 'order':
    navigation.navigate(
      'OrderDetails',
      {
        orderId: data.orderId,
      },
    );
    break;
}

Каждый новый тип уведомления соответствует новому случаю в операторе switch. Значения данных FCM представлены в виде строк, поэтому числа необходимо парсить явно.

Полная архитектура

Собирая все компоненты вместе, производственная конфигурация выглядит следующим образом:

┌──────────────────┐
                    │    Backend API   │
                    └────────┬─────────┘
                             │
                             ▼
                    ┌──────────────────┐
                    │ Firebase / FCM   │
                    └────────┬─────────┘
                             │
                ┌────────────┴────────────┐
                │                         │
                ▼                         ▼
        ┌──────────────┐          ┌──────────────┐
        │    Android   │          │     iOS      │
        └──────┬───────┘          └──────┬───────┘
               │                         │
               ▼                         ▼
        Notification                 APNs / FCM
          Channel                        │
               │                         │
               └────────────┬────────────┘
                            ▼
                    React Native App
                            │
                            ▼
                    Navigation / UI

На iOS FCM сам по себе не доставляет сообщения; он передает их через сервис Apple Push Notification (APNs), поэтому необходимо настроить учетные данные APNs.

Распространённые ошибки

Запрос информации при первом запуске

Такой подход на первом экране без каких-либо пояснений является самой распространённой ошибкой:

useEffect(() => {
  requestNotificationPermission();
}, []);

Сначала объясните значение, а затем запросите разрешение в ответ на действие пользователя.

Забывание о каналах

Без канала уведомления не появятся в Android 8.0 и новее версиях. Создайте канал перед отображением уведомлений:

await notifee.createChannel({
  id: 'messages',
  name: 'Messages',
});

Ожидание появления уведомлений на первом плане

Использование метода

messaging().onMessage(...)

не гарантирует появления видимого системного уведомления во всех случаях. Для стабильного отображения на первом плане используйте функцию Notifee.

Игнорирование изменений токена

Токен, сохранённый один раз, со временем перестаёт работать. Используйте

messaging().onTokenRefresh(...)

и отправляйте каждый новый токен на сервер.

Регистрация обработчиков в фоновом режиме в компонентах

Обработчик фона должен находиться в точке входа, вне дерева React, как показано ранее.

Игнорирование последующих изменений разрешений

Пользователи могут отзывать разрешения или блокировать отдельный канал в любое время. Ваши проверки должны учитывать каждый из этих состояний:

Granted
Denied
Changed externally
Channel blocked
App notifications disabled

Утечка конфиденциальных данных

Текст уведомления виден на экране блокировки и для всех, кто находится рядом с устройством. Вместо того чтобы

Your bank account ending in 1234 received ₹50,000.

отправлять что-то нейтральное:

New account activity
Tap to view details.

и показывать детали только после того, как пользователь авторизовался в приложении.

Чек-лист тестирования

Поведение зависит от версии ОС, состояния приложения и устройства, поэтому тестируйте систематически. На Android:

☐ Android 12
☐ Android 13+
☐ Permission granted
☐ Permission denied
☐ Permission disabled from Settings
☐ Channel enabled
☐ Channel disabled
☐ App foreground
☐ App background
☐ App killed
☐ Notification tapped
☐ Multiple notifications

На iOS:

☐ Permission granted
☐ Permission denied
☐ Permission changed from Settings
☐ App foreground
☐ App background
☐ App terminated
☐ Notification tapped
☐ Badge
☐ Sound
☐ Deep linking

А для полного тестирования интеграции FCM:

☐ Token generated
☐ Token stored on backend
☐ Token refresh handled
☐ Foreground message received
☐ Background message received
☐ Notification click handled
☐ Initial notification handled

Обзор потока выполнения

Полная последовательность действий, от запуска приложения до доставки уведомления и навигации, выглядит следующим образом:

APP START
                       │
                       ▼
              Check permissions
                       │
              ┌────────┴────────┐
              │                 │
           Allowed            Denied
              │                 │
              ▼                 ▼
       Initialize FCM      Show explanation
              │                 │
              ▼                 ▼
       Get FCM token       Request permission
              │
              ▼
       Send token to API
              │
              ▼
          App Running
              │
       ┌──────┴──────┐
       │             │
       ▼             ▼
   Foreground     Background
       │             │
       ▼             ▼
    onMessage      FCM/APNs
       │             │
       ▼             ▼
    Notifee       System UI
       │             │
       └──────┬──────┘
              ▼
        User taps
              │
              ▼
       Read notification data
              │
              ▼
          Navigation

Локальные уведомления против уведомлений через push и их совместное использование

Эти два типа уведомлений отличаются по нескольким предсказуемым критериям:

  • Источник: локальные уведомления поступают из приложения или устройства; уведомления через push — с вашего бэкенда.
  • Сеть: локальные уведомления обычно работают в офлайн-режиме; уведомления через push требуют подключения к сети.
  • FCM: не требуется для локальных уведомлений; обычно необходимо для уведомлений через push.
  • Запланированные напоминания: естественно подходят для локальных уведомлений; не являются типичным способом использования уведомлений через push.
  • События сервера, обновления заказов, сообщения в чате: уведомления через push — стандартный выбор; локальные уведомления могут обрабатывать их только ограниченным образом.
  • Библиотеки: Notifee для локальных уведомлений; FCM вместе с библиотекой отображения для уведомлений через push.

Они также хорошо сочетаются между собой. Типичная конфигурация предполагает, что FCM доставляет сообщение, ваш код обрабатывает его, а Notifee отображает его:

FCM receives message
        ↓
React Native processes message
        ↓
Notifee displays native notification

Что должны видеть пользователи на каждом этапе

Для ручной проверки важно знать, чего ожидать. В Android 13+ появляется системное диалоговое окно:

Allow MyApp to send you notifications?

В настройках системных уведомлений приложения каналы, которые вы создали, отображаются как отдельные переключатели:

MyApp
 ├── Messages
 ├── Orders
 └── Promotions

В iOS появляется запрос на разрешение от системы, а доставленные уведомления отображаются в Центре уведомлений. Для технологии push консоль Firebase может отправить тестовое сообщение непосредственно на токен устройства, что является самым быстрым способом отделения ошибок серверной части от ошибок клиентской части:

Firebase Console
      ↓
Cloud Messaging
      ↓
Test Message
      ↓
FCM Token

Наконец, при нажатии на уведомление о заказе:

Notification
     ↓
Order #12345

пользователь должен попасть на соответствующий экран:

Order Details Screen

Лучшие практики для производственных систем

Планируйте запросы на разрешения так, чтобы пользователи понимали, почему уведомления полезны, а не просто запрашивайте их при старте приложения. Храните логику в одном сервисе, а не дублируйте её на разных экранах:

notificationService.ts

Разделяйте типы уведомлений на каналы или категории, например:

messages
orders
payments
security
marketing

Разрабатывайте структуру данных для навигации, включая соответствующие поля там, где это необходимо:

type
id
screen

Кроме того:

  • Связывайте токены с авторизованными пользователями и устройствами, храните их в безопасности и обрабатывайте процедуру обновления.
  • Избегайте включения конфиденциальной информации в текст уведомлений, поскольку он виден вне приложения.
  • Тестируйте на реальных устройствах, так как эмуляторы и симуляторы могут вести себя по-разному.

Заключение

Рабочая система уведомлений — это гораздо больше, чем просто один вызов вроде

showNotification();

Это сочетание всех этих аспектов:

Permissions
     +
Notification Channels
     +
FCM Tokens
     +
Foreground Handling
     +
Background Handling
     +
Notification Clicks
     +
Deep Linking
     +
Backend Integration

Ключевой момент — четкое разделение обязанностей. FCM доставляет сообщения. Операционная система отвечает за разрешения и нативное отображение. Notifee предоставляет React Native контроль над локальным отображением. Ваш бэкенд решает, когда и что отправлять, а также кому. Если рассматривать это как единую систему, она выглядит следующим образом:

┌─────────────────┐
              │   Backend API   │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │      FCM        │
              └────────┬────────┘
                       │
              ┌────────┴────────┐
              ▼                 ▼
        ┌───────────┐     ┌───────────┐
        │  Android  │     │    iOS    │
        │  Channel  │     │   APNs    │
        └─────┬─────┘     └─────┬─────┘
              │                 │
              └────────┬────────┘
                       ▼
                React Native
                       │
                       ▼
                 Notifee
                       │
                       ▼
                Native UI

При наличии таких границ уведомления ведут себя предсказуемо как на Android, так и на iOS, в режимах фонового, переднего плана и после завершения работы приложения.

Краткий справочник

Запросите разрешение в Android:

await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);

Запросите разрешение в iOS:

await notifee.requestPermission();

Создайте канал в Android:

await notifee.createChannel({
  id: 'messages',
  name: 'Messages',
});

Отобразите уведомление:

await notifee.displayNotification({
  title: 'New Message',
  body: 'You have a new message.',
  android: {
    channelId: 'messages',
  },
});

Прослушивайте сообщения FCM в режиме переднего плана:

messaging().onMessage(async message => {
  console.log(message);
});

Прослушивайте изменения токена:

messaging().onTokenRefresh(token => {
  console.log(token);
});

Обработка уведомлений, открываемых из фона:

messaging().onNotificationOpenedApp(message => {
  console.log(message.data);
});