# Expo Recipe

Two ways to push an Expo app with Native Notify — the native-notify SDK on the Expo path, or the push API with the native APNs/FCM token from expo-notifications.

Expo apps have two supported routes:

1. **The Expo SDK path — start here.** Install `native-notify` and call `registerNNPushToken()`; the SDK registers Expo push tokens, and sends go through Expo's push service with receipts, inboxes and analytics. Follow [Installation and Setup](/docs/setup/installation).
2. **Universal push** — register the *native* APNs/FCM token yourself. Choose this when you want sends to go directly through your own Firebase project and Apple key (no Expo push service in the middle).

## Universal push in an Expo app

Get the native device token with `expo-notifications` and a stable device id with `expo-application` (`npx expo install expo-notifications expo-application`):

```js title="universalPush.js"
import * as Notifications from "expo-notifications";
import * as Application from "expo-application";
import { Platform } from "react-native";

const REGISTER_URL = "https://app.nativenotify.com/api/universal/device/register";

// Stable across launches: the vendor id on iOS, ANDROID_ID on Android.
async function stableDeviceId() {
  const id = Platform.OS === "ios" ? await Application.getIosIdForVendorAsync() : Application.getAndroidId();
  if (!id) throw new Error("No device id is available yet — retry on the next launch.");
  return id;
}

let lastRegisteredToken = null;

async function postRegistration({ appId, appToken, subscriberId }, devicePushToken) {
  const token = String(devicePushToken.data);
  if (token === lastRegisteredToken) return null; // already registered this session
  const res = await fetch(REGISTER_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      appId,
      appToken,
      deviceId: await stableDeviceId(),
      platform: Platform.OS,
      subscriberId, // your logged-in user's id; omit for an anonymous device
      tokens: Platform.OS === "ios" ? { apnsToken: token } : { fcmToken: token },
    }),
  });
  if (res.status !== 201) {
    // 400/403 answer { error: { code, message } }; a wrong appId/appToken answers 401 as plain text.
    throw new Error(`Native Notify register failed (${res.status}): ${await res.text()}`);
  }
  lastRegisteredToken = token;
  return res.json(); // { ok, device, tokens }
}

export async function registerUniversalDevice(args) {
  if (Platform.OS !== "ios" && Platform.OS !== "android") return null;
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== "granted") return null;

  // The NATIVE token: APNs on iOS, FCM on Android (development/EAS builds only).
  const devicePushToken = await Notifications.getDevicePushTokenAsync();
  return postRegistration(args, devicePushToken);
}

// Re-register when the native token rotates. Use the token the listener hands
// you: calling getDevicePushTokenAsync() inside this listener re-fires it.
export function watchUniversalToken(args) {
  const subscription = Notifications.addPushTokenListener((devicePushToken) => {
    postRegistration(args, devicePushToken).catch((err) => console.warn("Native Notify:", err));
  });
  return () => subscription.remove();
}
```

Call `registerUniversalDevice({ appId, appToken, subscriberId })` on every launch (an idempotent upsert — same `deviceId`, same device) and keep `watchUniversalToken(...)` subscribed while the app runs, so a rotated token is registered again. The app's [push credentials](/docs/push/credentials) are what make sends deliver.

`expo-notifications` displays these pushes on both platforms and hands your `pushData` to your listeners as `notification.request.content.data` — no extra rendering code is needed.

- **Android:** the build needs your Firebase config — `"android": { "googleServicesFile": "./google-services.json" }` in `app.json` — from the *same* Firebase project whose service-account JSON is saved in Native Notify (otherwise sends fail with `SENDER_ID_MISMATCH`).
- **iOS:** an EAS/development build with push enabled; Native Notify sends with the app's `.p8` key to its bundle id.

> **Expo Go:**
>
> `getDevicePushTokenAsync()` needs a development or EAS build. In Expo Go it throws on Android (SDK 53+), and on iOS the token it returns belongs to the Expo Go app — not yours — so your Apple key cannot deliver to it (`DeviceTokenNotForTopic`).

## Which route should we choose?

| You want…                                                                                                | Route                                                         |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| The fastest Expo setup, Notification Inbox components, receipts, analytics app-opens                     | **Expo SDK path** — `native-notify` + `registerNNPushToken()` |
| Sends straight through your own Firebase/Apple credentials, one contract shared with Flutter/native apps | **Universal push** — this page                                |
| Both features in different apps                                                                          | Both — the paths are independent per app                      |

## Next steps

- [Device Registration](/docs/push/registration) — every field and rule.
- [Verify Delivery](/docs/push/verification) — test-send to the device you just registered.
- [Send Notifications](/docs/push/sending) — blasts, device lists, subscriber lists.
