Push-уведомления, которые не приводят к отписке
Разрешение на уведомления пользователь даёт один раз. Разбираем, когда его просить, что отправлять и как не потерять канал за первую неделю.

Push-уведомления — единственный бесплатный канал, который достаёт пользователя без его запроса. Именно поэтому его так легко сжечь: разрешение выдаётся один раз, отзывается в два касания, и вернуть его почти невозможно. По нашему опыту приложение теряет от четверти до половины подписчиков на уведомления в первый месяц, и почти всегда — не из-за текста, а из-за частоты и бессмысленности сообщений.
Разберём канал с технической и продуктовой стороны: как получить разрешение, чем отличаются платформы, что отправлять и как понять по цифрам, что рассылка вредит.
Момент запроса разрешения
Самая частая ошибка — системный запрос на первом экране, до того как человек понял, зачем ему приложение. Соглашаются в этом случае единицы, а второго шанса система не даёт: на iOS повторно показать диалог нельзя, остаётся только вести пользователя в настройки, куда никто не ходит.
Рабочая схема — двухшаговый запрос. Сначала свой экран, который объясняет пользу конкретными словами: «пришлём, когда заказ передадут курьеру». Если человек согласился на своём экране, показываем системный диалог. Если отказался — не показываем и спрашиваем позже, после действия, где уведомления явно полезны.
import * as Notifications from "expo-notifications"
export async function askForPush(reason: "order_placed" | "price_alert") {
const current = await Notifications.getPermissionsAsync()
// Уже решили — второй раз системный диалог не показываем:
// на iOS он просто не появится, а счётчик попытки сгорит
if (current.status !== "undetermined") return current.status
// Свой экран с объяснением показываем до системного запроса
const agreed = await showPrePermissionSheet(reason)
if (!agreed) return "deferred"
const { status } = await Notifications.requestPermissionsAsync()
return status
}Чем различаются платформы
| Аспект | iOS | Android |
|---|---|---|
| Разрешение | Обязательное, запрос один раз | Обязательное с Android 13, раньше выдавалось само |
| Доставка | Через APNs, без гарантии времени | Через FCM, влияет режим экономии батареи |
| Тихие уведомления | Есть, с ограничением частоты | Есть, приоритет задаётся каналом |
| Группировка | По thread-id | По каналам, пользователь отключает их по отдельности |
| Отписка | Полностью или по типам, если настроено | По каналам: часть уведомлений можно оставить |
Каналы на Android — недооценённая возможность. Если развести уведомления по каналам «статус заказа», «скидки», «новости», пользователь отключит только раздражающий канал, а не приложение целиком. На iOS похожего механизма нет, поэтому типы уведомлений придётся заводить в своих настройках и уважать их на сервере.
Что отправлять
- Событие, которое пользователь ждёт: заказ собран, задача назначена, документ подписан
- Изменение по объекту, за которым он сам попросил следить: цена, статус, ответ в переписке
- Напоминание о незавершённом действии — не чаще одного раза на действие
- Регулярную сводку, если пользователь сам выбрал частоту
Всё остальное — рекламные рассылки «мы вас потеряли», акции без привязки к интересу, новости компании — работает против канала. Кратковременный подъём открытий такие уведомления дают, но за ними идёт волна отписок, после которой полезные сообщения о заказах доходят до меньшей аудитории.
Как считать эффект
Одного показателя открытий недостаточно: он растёт от агрессивных формулировок и ничего не говорит о вреде. Мы смотрим на четыре цифры в связке.
| Показатель | Что показывает | Тревожный сигнал |
|---|---|---|
| Доля разрешений | Качество запроса и объяснения | Меньше 40% на пользователей после первого действия |
| Открытия | Уместность конкретной рассылки | Ниже 3% при массовой отправке |
| Отписки за неделю | Вред от частоты | Больше 2% подписчиков в неделю |
| Удаления приложения | Крайняя реакция на рассылку | Рост после запуска новой серии уведомлений |
const LIMITS = {
order: { perDay: 10 }, // транзакционные — почти без ограничений
reminder: { perDay: 1 },
marketing: { perWeek: 2 },
}
export async function canSend(userId: string, kind: keyof typeof LIMITS) {
const prefs = await getPrefs(userId)
if (!prefs.enabled[kind]) return false
const limit = LIMITS[kind]
const since = limit.perDay ? hoursAgo(24) : daysAgo(7)
const sent = await countSent({ userId, kind, since })
return sent < (limit.perDay ?? limit.perWeek)
}Ограничение частоты должно жить на сервере, а не в интерфейсе рассылок. Иначе рано или поздно кто-то запустит кампанию в обход правил — и канал придётся восстанавливать месяцами.
Что мы делаем на проектах
- Разделяем уведомления на транзакционные и маркетинговые с раздельными настройками
- Просим разрешение после первого значимого действия, со своим экраном-объяснением
- Заводим лимиты частоты на сервере и логируем каждую отправку
- Складываем в аналитику отправку, доставку, открытие и отписку одним событием
- Раз в месяц смотрим отписки по типам и отключаем то, что не окупается
Частые вопросы
- Сколько стоит подключить push-уведомления
- Базовая интеграция с APNs и FCM, настройки в приложении и серверная отправка — это обычно 20–40 часов работы. Сложность добавляют сегментация, расписания и админка для рассылок: с ними объём вырастает вдвое.
- Можно ли обойтись без своего сервера
- Для простых рассылок хватает готовых сервисов вроде Firebase или OneSignal. Своя логика нужна, когда уведомления зависят от данных в вашей системе: статуса заказа, остатка на складе, назначения задачи.
- Что делать, если пользователь отказался от уведомлений
- Не показывать системный диалог повторно — он не появится. Оставьте в настройках приложения понятный переключатель со ссылкой в системные настройки и предложите альтернативу: письмо или сообщение в мессенджер.
Похожая задача в вашем проекте?
Разберём вашу ситуацию и пришлём оценку по этапам — без обязательств и общих слов.
Обсудить задачу

