Expo Push Notifikace v React Native 2026: Kompletní průvodce s FCM v1 a APNs

Nastavení Expo push notifikací v React Native pro rok 2026: FCM v1 service account, APNs P8 klíč, získání tokenu, odesílání přes Expo Push API a řešení nejčastějších chyb na Androidu i iOS.

Aktualizováno: 19. srpna 2026

Expo push notifikace v React Native 2026 nastavíte tak, že do projektu přidáte balíček expo-notifications, vytvoříte development build (Expo Go už pro remote push na Androidu nestačí), načtete Expo push token přes getExpoPushTokenAsync, nahrajete FCM v1 service account do EAS a APNs klíč pro iOS, a zprávy pak posíláte z vlastního backendu jedním POST požadavkem na https://exp.host/--/api/v2/push/send. Celý pipeline se od dob FCM Legacy citelně změnil, a bez FCM HTTP v1 už Android push zkrátka nefunguje.

  • FCM Legacy API bylo definitivně vypnuto v září 2024. Android push v roce 2026 vyžaduje FCM HTTP v1 se service account klíčem nahraným do EAS Credentials.
  • Remote push notifikace už na Androidu nefungují v Expo Go (od SDK 53). Pro testování potřebujete development build nebo EAS Build.
  • Doporučené verze pro rok 2026: expo@~53 nebo novější a expo-notifications@~0.32 s podporou FCM v1 a nového notifikačního response API.
  • Expo Push Service je zdarma a agnostický. Jeden POST request s až 100 tokeny doručí zprávu na iOS i Android, receipty se pak ověřují po ~15 minutách.
  • Na Androidu 13+ musíte explicitně požádat o POST_NOTIFICATIONS a vytvořit notifikační kanál, jinak systém zprávu tiše zahodí.
  • Pro deep linking pracujte s data payloadem a Notifications.addNotificationResponseReceivedListener spolu s Expo Routerem, ne s URL v title nebo body.

Jak fungují Expo push notifikace: pohled webaře

Když jsem přecházela z webu na React Native, největší mentální blok byl právě u push notifikací. Ve webovém prohlížeči máte Push API plus service workera, jednu VAPID klíčenku a v podstatě jednu abstraktní vrstvu; browser to za vás doručí. Na mobilu je model rozdělený: iOS má Apple Push Notification service (APNs), Android má Firebase Cloud Messaging (FCM). Každá platforma má vlastní autentizaci, vlastní payload formát, vlastní politiky doručování i vlastní nástroje pro debug. Přesně tenhle šev balíček expo-notifications zakrývá.

Expo Push Service funguje jako proxy. Vy pošlete jeden POST s ExpoPushToken na exp.host, Expo si podle prefixu tokenu vybere, jestli jde o APNs (iOS zařízení) nebo FCM (Android), a přepošle payload. Získáte tak stejnou jednoduchost, jakou znáte z webového Push API, ale s tou zásadní výhodou, že příjemce nemusí mít otevřený tab. Systém probudí i zabitou aplikaci.

Rozdíly, které přechod z webu opravdu bolí? Neexistuje tady ServiceWorkerRegistration.showNotification(), kterou by se dala notifikace vykreslit v odpovědi na push. Na Androidu se o vykreslení stará systém přímo z payloadu, pokud pošlete notification objekt ve FCM zprávě. Když přijde jen data payload, aplikace ho může zpracovat na pozadí přes expo-task-manager, ale vykreslení notifikace už si musíte objednat ručně přes Notifications.scheduleNotificationAsync. Na iOS je to podobné. Silent push (content-available: 1) probudí aplikaci na maximálně 30 sekund, což je omezení, které z webu neznáte.

Instalace balíčků a základní nastavení projektu

Předpokládám, že máte Expo SDK 53 nebo novější a projekt spravovaný přes EAS. Do package.json potřebujete tři balíčky: expo-notifications pro samotné API, expo-device pro detekci fyzického zařízení (na simulátoru remote push nefunguje) a expo-constants pro přístup k projectId, ze kterého se generuje Expo push token.

npx expo install expo-notifications expo-device expo-constants

Následně do app.json (nebo app.config.ts) přidejte config plugin. Bez něj se na iOS nevytvoří entitlement aps-environment a APNs vám během build fáze prostě mlčky selže.

{
  "expo": {
    "plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#0F172A",
          "defaultChannel": "default",
          "sounds": ["./assets/notification.wav"]
        }
      ]
    ],
    "android": {
      "googleServicesFile": "./google-services.json",
      "package": "com.example.app",
      "useNextNotificationsApi": true
    },
    "ios": {
      "bundleIdentifier": "com.example.app",
      "infoPlist": {
        "UIBackgroundModes": ["remote-notification"]
      }
    }
  }
}

Ikona notifikace na Androidu musí být monochromatická (bílá silueta s průhledným pozadím). Barevná ikonka se zobrazí jako bílý čtverec, a je to jedna z nejčastějších chyb, které v issues vidím. Pokud pracujete s vlastními zvuky, počítejte s tím, že iOS akceptuje jen .wav, .caf nebo .aiff, a kratší než 30 sekund. Až budete mít config plugin, spusťte npx expo prebuild --clean, ať se změny propíšou do nativních projektů, a pak eas build --profile development --platform all.

Získání Expo push tokenu a oprávnění na Androidu 13+

Push token je unikátní identifikátor konkrétní instalace aplikace. Musíte ho poslat na svůj backend a uložit ke správnému uživateli, jinak nemáte komu notifikaci doručit. Následující hook zapouzdřuje celou registraci: vyžádá si oprávnění, ověří platformu, na Androidu založí notifikační kanál a vrátí ExpoPushToken.

import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export function usePushRegistration() {
  const [token, setToken] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    (async () => {
      if (!Device.isDevice) {
        setError('Remote push funguje jen na fyzickém zařízení.');
        return;
      }

      if (Platform.OS === 'android') {
        await Notifications.setNotificationChannelAsync('default', {
          name: 'Výchozí',
          importance: Notifications.AndroidImportance.HIGH,
          vibrationPattern: [0, 250, 250, 250],
          lightColor: '#0F172A',
        });
      }

      const { status: existing } = await Notifications.getPermissionsAsync();
      let finalStatus = existing;
      if (existing !== 'granted') {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
      if (finalStatus !== 'granted') {
        setError('Uživatel odmítl oprávnění pro notifikace.');
        return;
      }

      const projectId =
        Constants.expoConfig?.extra?.eas?.projectId ??
        Constants.easConfig?.projectId;

      const pushToken = await Notifications.getExpoPushTokenAsync({ projectId });
      setToken(pushToken.data);
    })().catch((e) => setError(String(e)));
  }, []);

  return { token, error };
}

Od Androidu 13 (API 33) musí aplikace explicitně žádat o POST_NOTIFICATIONS runtime permission, a to funkce requestPermissionsAsync udělá za vás. Bez notifikačního kanálu Android systémově zprávy zahodí, aniž by jakkoli signalizoval chybu. Volání setNotificationChannelAsync je proto povinný krok, ne volitelný. Já jsem přesně na tenhle bug narazila ve svém posledním projektu: iOS jsem si otestovala hned, Android push mi celý týden mlčel, než mi kolega ukázal, že jsem zapomněla vytvořit kanál. Na iOS se během requestPermissionsAsync zobrazí systémový dialog, který uživatel může odmítnout, a podruhé už ho nezobrazíte. Uživatel musí do Nastavení ručně. Pro produkci si přichystejte lehký "priming" onboarding, který ospravedlní důvod, proč oprávnění potřebujete.

Konfigurace FCM v1 pro Android v roce 2026

Tak, tohle je krok, který v roce 2026 musíte udělat, a přitom je nejčastějším bodem selhání. Google k 22. červnu 2024 vypnul FCM Legacy API a od září 2024 už neběží ani přechodná období. Expo se přeorientovalo na FCM HTTP v1, který místo statického server key vyžaduje OAuth 2.0 přes service account. Podrobnou timeline najdete v migračním blogpostu Expo.

Postup je následující:

  1. Ve Firebase Console vytvořte projekt a v Project Settings → General přidejte Android aplikaci s odpovídajícím applicationId. Stáhněte google-services.json a umístěte do rootu projektu.
  2. Přejděte do Project Settings → Service Accounts, klikněte na Generate new private key. Firebase vygeneruje JSON soubor, což je credential pro FCM HTTP v1.
  3. Nahrajte JSON do EAS credentials: eas credentials, vyberte Android, poté FCM V1 service account key a nahrajte soubor. Alternativně přes web UI na expo.dev.
  4. Ověřte spuštěním eas credentials --platform android. Měl by se objevit řádek FCM V1 service account key: configured.
# Rychlá kontrola, že projekt používá FCM v1
eas credentials --platform android

# Test odeslání konkrétnímu tokenu z CLI (bez psaní vlastního backendu)
curl -H "Content-Type: application/json" \
     -X POST "https://exp.host/--/api/v2/push/send" \
     -d '{
       "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxx]",
       "title": "Test",
       "body": "FCM v1 funguje"
     }'

Nastavení APNs pro iOS a Apple Developer účet

Pro iOS potřebujete placený Apple Developer účet (99 USD/rok) a APNs authentication key ve formátu .p8. Doporučuji jednoznačně token-based auth (P8), ne staré certifikáty (P12). Klíč P8 nikdy neexpiruje, funguje pro všechny appky pod team ID, a nemusíte ho ročně obnovovat. Kdo si toto zjednodušení jednou vyzkoušel, k P12 se dobrovolně nevrátí.

  1. V Apple Developer → Keys klikněte na +, pojmenujte klíč (např. ExpoPushKey), zaškrtněte Apple Push Notifications service (APNs) a vygenerujte. Stáhne se soubor AuthKey_XXXXXXXXXX.p8. Uložte ho hned, nelze stáhnout podruhé.
  2. Poznamenejte si Key ID (10 znaků) a Team ID (najdete v pravém horním rohu Developer účtu).
  3. Spusťte eas credentials --platform ios, vyberte Push Notifications: Manage your Apple Push Notifications Key a nahrajte P8 spolu s Key ID a Team ID.
  4. Při build fázi eas build --platform ios se entitlement aps-environment=production propíše do provisioning profilu automaticky.

Pro sandbox testování (development build) Expo použije APNs sandbox endpoint, pro release APNs production. Pozor: token vygenerovaný pod dev buildem nefunguje po nasazení do TestFlight nebo App Store, token se prostě změní. Backend si proto musí umět uchovat oba tokeny nebo dostat token znovu při první produkční instalaci.

Odesílání zpráv přes Expo Push API a ověření receiptů

Expo Push API je HTTPS endpoint https://exp.host/--/api/v2/push/send, který přijímá pole zpráv (max 100 na request). Doporučuji zapouzdřit ho do vlastní služby na backendu. Nikdy neposílejte notifikace přímo z klienta, jinak vám unikne přístupový token a někdo by mohl spammovat vaše uživatele.

// Node.js server (Bun/Deno funguje stejně)
import type { ExpoPushMessage, ExpoPushTicket } from 'expo-server-sdk';
import { Expo } from 'expo-server-sdk';

const expo = new Expo({ useFcmV1: true });

export async function sendBatch(tokens: string[], payload: Omit<ExpoPushMessage, 'to'>) {
  const messages: ExpoPushMessage[] = tokens
    .filter((t) => Expo.isExpoPushToken(t))
    .map((to) => ({
      to,
      sound: 'default',
      priority: 'high',
      channelId: 'default', // Android
      ...payload,
    }));

  const chunks = expo.chunkPushNotifications(messages);
  const tickets: ExpoPushTicket[] = [];
  for (const chunk of chunks) {
    const chunkTickets = await expo.sendPushNotificationsAsync(chunk);
    tickets.push(...chunkTickets);
  }
  return tickets;
}

Vrácený ticket zatím jen říká, že Expo požadavek přijalo. Skutečné doručení APNs nebo FCM zjistíte až přes receipts. Nasbírejte ticketId z ticketů se stavem ok, počkejte 15 až 30 minut a zavolejte getPushNotificationReceiptsAsync. Receipt vrátí ok, error nebo detail typu DeviceNotRegistered, MessageTooBig, MessageRateExceeded. Podle Expo docs by tokeny s chybou DeviceNotRegistered měly být okamžitě smazány z vaší databáze, jinak si u FCM koledujete o rate-limiting a v extrémním případě o dočasný ban vašeho APNs klíče.

Payload struktura, na kterou se ptají nejčastěji: title, body, data, sound, badge, ttl, priority (default/high) a specificky pro Android channelId. Pole data je JSON, který dorazí do aplikace. Sem patří ID objektů, deep-link cíle a cokoli, co potřebujete pro navigaci.

Zpracování přijaté notifikace a deep linking

Notifikace může přijít ve třech různých stavech aplikace, a každý má vlastní API:

  • Foreground (aplikace otevřená): addNotificationReceivedListener. Rozhoduje setNotificationHandler, jestli se banner vůbec zobrazí.
  • Background (uživatel klepl na notifikaci): addNotificationResponseReceivedListener. Sem patří vaše navigace na cílovou obrazovku.
  • Cold start (aplikace zabitá, notifikace ji spustila): getLastNotificationResponseAsync(). Asynchronní čtení posledního response ještě před inicializací routeru.
import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';
import { router } from 'expo-router';

export function useNotificationRouting() {
  const responseListener = useRef<Notifications.Subscription>();

  useEffect(() => {
    // Cold start: aplikace se spustila klepnutím na notifikaci
    Notifications.getLastNotificationResponseAsync().then((response) => {
      if (response) route(response);
    });

    // Warm start: uživatel klepl při běžící aplikaci
    responseListener.current = Notifications.addNotificationResponseReceivedListener(route);

    return () => {
      responseListener.current?.remove();
    };
  }, []);
}

function route(response: Notifications.NotificationResponse) {
  const data = response.notification.request.content.data as {
    screen?: string;
    id?: string;
  };
  if (data.screen === 'article' && data.id) {
    router.push(`/article/${data.id}`);
  }
}

Pokud používáte souborový routing, doporučuji přečíst si našeho průvodce Expo Routerem pro rok 2026. Pattern router.push(path) se skvěle skládá s data payloadem push notifikace a dovolí vám poslat uživatele hluboko do navigačního stromu bez jakéhokoli custom URL parseru. Klíčové je nikdy nedávat cílovou URL do title nebo body. Ty jsou pro člověka, ne pro váš router.

Fungují push notifikace v Expo Go a jak řešit časté chyby

Krátká odpověď: ne, remote push notifikace už v Expo Go plně nefungují. Od Expo SDK 53 (a v roce 2026 už dávno) je Expo Go pro remote push na Androidu neplatná cesta. Sdílené FCM credentials Expo Go byly zrušeny právě kvůli přechodu na FCM HTTP v1, který vyžaduje service account per-projekt. Na iOS byl Expo Go i historicky omezený jen na foreground notifikace. Řešením je development build vytvořený přes eas build --profile development. Ten je funkčně identický s Expo Go (podporuje QR nahrávání JS bundlu), ale obsahuje vaše nativní credentials.

Nejčastější chyby, které vidím v Discord Expo v roce 2026:

  • InvalidCredentials: FCM v1 service account má expirovanou nebo revokovanou private key. Vygenerujte novou v Firebase a znovu nahrajte do EAS.
  • Notifikace přijde na iOS, ale ne na Androidu: chybí channelId v payloadu, nebo aplikace nikdy nezavolala setNotificationChannelAsync. Android bez kanálu tiše zahodí.
  • Bílý čtverec místo ikony na Androidu: ikona není monochromatická. Musí být bílá silueta na průhledném pozadí, ideálně 96×96 px.
  • Notifikace nedorazí, když je app zabitá na Androidu: některé Android skiny (Xiaomi MIUI, Huawei EMUI, OPPO ColorOS) agresivně killují background procesy. Uživatel musí ručně povolit "autostart", není to bug ve vašem kódu.
  • MessageTooBig: payload musí být pod 4 KB. Držte data minimalistické, posílejte ID, ne celý objekt.
  • Simulátor iOS nedostává remote push: od Xcode 14 je to podporované, ale musíte provést test přes xcrun simctl push s .apns souborem. Expo Push API na simulátor doručit neumí.

Často kladené otázky

Kolik stojí Expo Push Notifikace v roce 2026?

Expo Push Service je zdarma bez limitu. Expo účtuje pouze EAS Build minuty a EAS Update bandwidth. Za samotné doručení FCM ani APNs neúčtují nic (do jejich vlastních free tier). Pro indie i větší produkci se v roce 2026 pohybujete v nulových nákladech na doručení notifikací.

Musím používat Expo Push Service, nebo mohu volat FCM a APNs přímo?

Přímé volání je plně podporované. Místo getExpoPushTokenAsync zavolejte getDevicePushTokenAsync a dostanete nativní FCM registration ID nebo APNs device token. Pak si sami implementujte HTTP v1 pro Android a APNs HTTP/2 pro iOS. Pro většinu projektů je to zbytečná složitost, ale pro enterprise se striktními audit požadavky to smysl dává.

Jak otestuji push notifikace na simulátoru iOS?

Od Xcode 14 to lze přes xcrun simctl push <booted> com.example.app payload.apns, kde payload.apns je JSON soubor s klíči aps.alert.title, aps.alert.body a případně data. Expo Push API na simulátor přímo doručit nedokáže, token je jiného typu. Pro end-to-end test remote push potřebujete fyzické zařízení.

Jaký je rozdíl mezi Expo Push Tokenem a device push tokenem?

ExpoPushToken je opaque identifikátor formátu ExponentPushToken[...], kterému rozumí jen Expo Push Service. Device push token je nativní token, na iOS 64-znakový hex APNs token, na Androidu FCM registration ID. Expo token je snazší, ale rezignujete na jemné ovládání specifické pro platformu (silent push, mutable-content, atd.).

Jak zvládnout notifikace při běžícím Fabric rendereru v Nové architektuře?

expo-notifications od verze 0.32 plně podporuje Novou architekturu (Fabric plus TurboModules). Klíčové je používat expo@~53 nebo novější, spustit npx expo prebuild --clean po povolení nové architektury a ověřit, že notifikační listenery jsou registrované uvnitř useEffect a ne v render fázi, jinak se v concurrent renderu mohou spustit vícekrát.

Anita Iyer
O Autorovi Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.