Жалоба «пуши не приходят» — одна из самых частых после запуска приложения. Неприятность в том, что почти все причины молчаливые: приложение работает, ошибок нет, сервер отвечает «отправлено», а на телефоне тишина. Ниже — порядок проверки, который мы прогоняем сами, от самого частого к редкому.
Сначала — доходит ли до устройства токен
Прежде чем что-то отправлять, приложение должно получить токен устройства и передать его на сервер. Половина случаев «не приходят пуши» заканчивается здесь: токен не получен или не сохранён.
Проверяется просто: посмотрите в базе, сколько записей пользователей имеет непустой токен. Если из сотни клиентов токен есть у нуля — отправлять некуда, и остальные проверки бессмысленны.
Две ловушки, на которые мы натыкались вживую. Первая: колонка под токен заведена как varchar(75), а токен FCM бывает длиной 140–190 символов — база молча обрезает его, и отправка уходит на несуществующий адрес. Вторая: эндпоинт сохранения токена объявлен публичным, идентификатор пользователя в него не передаётся, токен пишется «в никуда» и к аккаунту не привязывается.
iOS: есть ли ключ APNs в проекте
Для iPhone одного Firebase мало. В проекте Firebase должен лежать ключ APNs — тот самый .p8, который выпускается в аккаунте Apple Developer. Без него getToken() просто не возвращает токен, а в консоли не появляется ни одной ошибки.
Особенно легко это пропустить, когда несколько приложений живут в одном проекте Firebase: ключ забыли добавить один раз — пуши мертвы у всех приложений проекта сразу. Проверить можно за минуту: настройки проекта, вкладка Cloud Messaging, раздел конфигурации приложений Apple.
Второй нюанс — порядок вызовов. Токен FCM нужно запрашивать после того, как получен токен APNs, иначе на части устройств запрос вернёт пустоту. И третий: на симуляторе токен не выдаётся вообще, проверять пуши можно только на живом устройстве или через TestFlight.
Разрешение от пользователя
С iOS 12 и Android 13 уведомления требуют явного согласия. Если приложение спросило разрешение в неудачный момент — на первом же экране, до того как человек понял, зачем оно ему, — большинство нажимает «Не разрешать». Дальше система молча отбрасывает все уведомления, и никакая настройка сервера этого не изменит.
Хорошая практика: спрашивать разрешение в момент, когда польза очевидна. Оформил заказ — «сообщить, когда курьер выедет?». Записался на приём — «напомнить за час?». Конверсия согласий вырастает в разы против запроса на старте.
Сервер: что именно отправляется
Если токены есть и разрешение получено, проверяем саму отправку. Здесь встречается неочевидное: пустое поле data в сообщении FCM даёт ответ 400, а функция отправки нередко написана так, что молча возвращает «не отправлено» и никуда об этом не пишет.
Полезное правило: логировать каждую попытку отправки вместе с ответом сервиса. Тогда вопрос «почему не пришло» решается просмотром журнала, а не гаданием.
Проверка одной командой
Самый быстрый способ отделить «сервер не шлёт» от «телефон не принимает» — отправить пуш напрямую через API сервиса на конкретный токен, минуя ваш бэкенд. Пришло — значит проблема в вашем коде отправки. Не пришло — проблема в токене, ключах или разрешениях.
Что делать по порядку
Смотрим базу: есть ли токены и не обрезаны ли они. Смотрим Firebase: лежит ли ключ APNs. Смотрим приложение: когда спрашивается разрешение и на живом ли устройстве проверяем. Смотрим сервер: пишется ли журнал отправок и что отвечает сервис. В девяти случаях из десяти причина находится на первых двух шагах.
Если разбираться некогда — напишите нам: мы делаем такую диагностику по чужому коду, обычно она занимает несколько часов и заканчивается конкретным списком правок.
Android: свои особенности
На Android к общим причинам добавляются фирменные оболочки. Xiaomi, Huawei, Oppo, Vivo и Samsung по-своему экономят батарею и умеют выгружать приложение из памяти вместе с его соединением для уведомлений. Приложение при этом установлено, разрешение выдано, но пуши приходят с задержкой в часы или не приходят вовсе.
Лечится это не кодом, а настройкой на устройстве: приложение нужно исключить из оптимизации батареи и разрешить автозапуск. Единого способа сделать это программно нет — у каждого производителя свой экран настроек. Разумный подход: если приложение критично зависит от уведомлений, показать пользователю подсказку при первом запуске, как разрешить автозапуск на его модели.
Вторая особенность Android — каналы уведомлений. С Android 8 каждое уведомление относится к каналу, и настройки канала задаются один раз при создании. Если канал создан со звуком «без звука», поменять это позже из кода нельзя — только пересоздать канал с новым идентификатором. Мы видели приложения, где уведомления приходили молча именно из-за этого.
Тихие и видимые уведомления
Сообщение FCM бывает двух типов, и путаница между ними даёт странное поведение. Уведомление с полем notification система показывает сама, даже когда приложение закрыто, — но обработать его в коде вы не сможете, пока пользователь не нажмёт. Сообщение только с полем data приложение обрабатывает само, но если оно выгружено из памяти, показать уведомление некому.
Практический вывод: для обычных оповещений («заказ готов», «пришло сообщение») отправляйте notification — так надёжнее. Для фоновых задач вроде обновления данных — data. Смешивать оба поля в одном сообщении можно, но поведение на разных версиях систем отличается, и это источник трудноуловимых различий между iPhone и Android.
Что писать в уведомлении
Техническая часть — половина дела. Вторая половина в том, что человек может отключить уведомления вашего приложения одним движением, и вернуть его почти невозможно. Отключают за навязчивость: три сообщения в день про акции, уведомления ночью, текст без смысла вроде «У нас новости!».
Рабочее правило: уведомление должно сообщать то, чего человек ждёт. Статус заказа, ответ на его сообщение, напоминание о записи. Рекламные рассылки лучше выносить в отдельный канал, который можно выключить, не теряя важное. И обязательно учитывать часовой пояс: сообщение в три часа ночи стоит вам отключённых уведомлений навсегда.