Expo SDK
Last updated
@whisperr/expo adds push notifications to an Expo app on Whisperr. It
builds on @whisperr/react-native, which sends
identify calls and events. Use this page to install both, identify users with
their email address, track events, and set up push.
@whisperr/expo has three parts:
- A config plugin. It sets up native push at prebuild: the iOS push entitlement, and the Android notification icon, color and default channel.
registerForPushNotifications(). It asks for permission, gets the Expo push token and sends both to Whisperr. It keeps them current.useWhisperrNotificationResponse(). It recordspush_openedwhen the user taps a Whisperr notification and gives you the deep link.
Using a coding agent? Connect it to the Whisperr MCP server. It installs the SDK and wires your events and push for you.
Install
Section titled “Install”npx expo install @whisperr/expo @whisperr/react-native expo-notifications expo-constants expo-application @react-native-async-storage/async-storageThe packages are @whisperr/expo
and @whisperr/react-native
on npm. @whisperr/expo needs Expo SDK 52 or later and
@whisperr/react-native 0.5.0 or later.
Add the plugin to your app config. With app.json, npx expo install may
already have added "@whisperr/expo" to plugins: add the options to that
entry. With app.config.js or app.config.ts, add it yourself:
{ "expo": { "plugins": [ ["@whisperr/expo", { "icon": "./assets/notification-icon.png", "color": "#4F6BFF" }] ], "android": { "googleServicesFile": "./google-services.json" } }}Then make a new build (eas build, or npx expo run:ios and
npx expo run:android). A config plugin changes native code, so a
JavaScript-only update is not enough.
At prebuild, the plugin warns when push cannot work: no
android.googleServicesFile, or no extra.eas.projectId (run eas init).
Plugin options
Section titled “Plugin options”| Option | Platform | Default | What it does |
|---|---|---|---|
mode | iOS | development | The aps-environment entitlement. App Store and TestFlight builds are re-signed for production. |
icon | Android | none | Small notification icon: a 96×96 all-white PNG with transparency. |
color | Android | none | Icon tint. |
androidChannelId | Android | default | The channel FCM uses when a message names none. registerForPushNotifications() creates this channel. |
sounds | both | none | Custom notification sounds. |
enableBackgroundRemoteNotifications | iOS | false | Adds remote-notification to UIBackgroundModes (silent pushes). |
The plugin applies the expo-notifications plugin for you. If your app config
already lists expo-notifications, your entry and its options win.
Get an app key
Section titled “Get an app key”Create an App key (wpk_…, the publishable key) in the Whisperr dashboard
under Settings → API keys. During setup you can also add one on Home →
Connect your code → Copy prompt with Add an app key. After setup, the
prompt is in Integrations → Your code → Your app → Copy a prompt, where
Get an app key opens the API keys page.
The app key ships in your app bundle by design. Never put a server key
(wrk_…) in app code.
Initialize and identify
Section titled “Initialize and identify”import { Whisperr } from "@whisperr/react-native";
// Uses AsyncStorage automatically when it is installed.const whisperr = Whisperr.init({ apiKey: "wpk_your_publishable_key" });Call identify after sign-up, after login, and on every app start when a
user is signed in. Send the email address as a channel, never as a trait:
whisperr.identify(user.id, { traits: { first_name: user.firstName }, channels: [{ type: "email", address: user.email, optedIn: user.agreedToEmail }],});App-side consent needs @whisperr/react-native 0.6.0 or later. Older
versions cannot set it: Whisperr stores the address as pending.
The email and emailOptedIn shortcut sends the same channel:
whisperr.identify(user.id, { email: user.email, emailOptedIn: user.agreedToEmail }).
Leave emailOptedIn out when the app does not know.
Can the app opt the user in to email?
Section titled “Can the app opt the user in to email?”It depends on your answer to Does your signup get permission to email users?, which Whisperr asks during setup:
| Answer | An email address the app sends |
|---|---|
| Yes | Is opted in, unless the app sends optedIn: false. |
| Per user | Is opted in only when the app sends optedIn: true. |
| No, or not answered | Is stored as pending. Your backend confirms it with the server key. |
When Require a server key is on, only your server can opt an address in, whatever the answer. Some limits protect the publishable key, which anyone can read from your app. For example, the app cannot change a user’s existing address. See Contact consent for the full rules.
optedIn: false always records an opt-out. An earlier opt-out, including the unsubscribe link and bounce or complaint
suppression, is kept. A later identify from the app never opts the address
back in.
On logout, call whisperr.reset().
Track events
Section titled “Track events”// An example name. Use your app's own action.whisperr.track("recipe_saved", { source: "search" });Event names must be lowercase snake_case. Do not send personal data, such
as an email address or a name, in event properties. track() works before
identify(): the next identify() moves those events to the user. See
Event design and the
React Native SDK for screens, automatic events
and options.
Register for push
Section titled “Register for push”Connect the Expo Push provider in the Whisperr dashboard first. See Connect a push provider.
import { registerForPushNotifications } from "@whisperr/expo";
// After identify, at the moment you want the OS prompt:const { status, token, reason } = await registerForPushNotifications();It does this:
- On Android, it creates the notification channel. Android 13 and later show the prompt only when a channel exists.
- It reads the permission and, if the app may still ask, shows the prompt.
- It reports the permission with
whisperr.setPushPermission(status). - If notifications are allowed, it gets the Expo push token and calls
whisperr.setPushToken()withkind: "expo", the EAS project id and the app id. Whisperr uses the two ids to catch a token from the wrong project or app before a send. - It keeps watching. Each time the app comes to the foreground it checks
the permission again, and it gets a new token when the device token
changes.
stopPushNotificationUpdates()stops this.
It never throws. When there is no token, reason says why:
reason | Meaning |
|---|---|
permission_denied | The user turned notifications off. |
permission_undetermined | The app did not ask (requestPermission: false) or the user did not answer. |
expo_go_android | Expo Go on Android cannot receive remote push (Expo SDK 53 and later). Use a development build. |
missing_project_id | No EAS project id. Run eas init or pass projectId. |
token_unavailable | The OS gave no token, often on a simulator or emulator. See error. |
not_initialized | Whisperr.init() did not run and you passed no client. |
Options: client, projectId, tokenType ("expo" by default, or
"device"), providerProject, requestPermission (default true),
provisional (quiet iOS delivery without a prompt), ios (prompt options),
androidChannel ({ id, name, importance } or false), watch (default
true) and debug.
With tokenType: "device", the SDK sends the native token instead: an APNs
token on iOS, an FCM token on Android. Use it when you send through your own
APNs key or Firebase project, and pass your Firebase project id as
providerProject.
Credentials
Section titled “Credentials”Whisperr sends Expo tokens through Expo’s push service. Expo needs:
- iOS: an APNs key.
eas buildsets it up, or runeas credentials. - Android: an FCM V1 service account key, uploaded with
eas credentials, andgoogle-services.jsonin the app config.
See Expo: push notifications setup.
Notification taps and deep links
Section titled “Notification taps and deep links”import { router, Stack } from "expo-router";import { useEffect } from "react";import { deepLinkToHref, useWhisperrNotificationResponse } from "@whisperr/expo";
export default function RootLayout() { const open = useWhisperrNotificationResponse();
useEffect(() => { const href = open?.deepLink ? deepLinkToHref(open.deepLink, "myapp") : null; if (href) router.push(href); }, [open]);
return <Stack />;}- It sends
push_openedonce per message, also for the tap that launched the app. Taps on notifications from other senders are ignored. - It also sends
push_receivedonce per message for Whisperr notifications that arrive while the app is in the foreground. PasstrackReceived: falseto turn this off. deepLinkToHref(link, scheme?)turnsmyapp://offers/annualorhttps://example.com/offers/annualinto/offers/annual. Withscheme, it accepts only links with your app’s scheme.
Outside React, use subscribeToWhisperrNotificationOpens(onOpen) and
subscribeToWhisperrNotificationReceipts(). Each returns an unsubscribe
function.
With @whisperr/react-native 0.7.0 or later, the tap that opens the app also
sets launch_source: "push" (and whisperr_message_id) on that launch’s
app_opened. See Launch source.
Screens with expo-router
Section titled “Screens with expo-router”From @whisperr/expo 0.2.0:
import { Stack } from "expo-router";import { useWhisperrScreens } from "@whisperr/expo";
export default function RootLayout() { useWhisperrScreens(); return <Stack />;}It sends screen_viewed each time the route changes, with the route template
as screen_name: app/(tabs)/lesson/[id].tsx is /lesson/[id], never
/lesson/42. Groups such as (tabs) are left out, and a repeat of the same
template is skipped. expo-router is an optional peer: without it the hook
does nothing. For React Navigation, use
whisperrNavigationTracker() from
@whisperr/react-native.
Expo Go
Section titled “Expo Go”- Events work in Expo Go.
- Remote push does not work in Expo Go on Android (Expo SDK 53 and later). Use a development build.
- Simulators and emulators often give no push token. Test push on a real device.
Common questions
Do I need @whisperr/expo, or is @whisperr/react-native enough?
@whisperr/react-native sends identify calls and events, also in Expo. @whisperr/expo adds push for Expo apps: a config plugin, registerForPushNotifications() and a hook for notification taps. Install both when you want push.
Can an Expo app without a backend email its users?
Yes, when you told Whisperr that your signup gets permission to email users, and the app uses @whisperr/react-native 0.6.0 or later. Then an email address that the app sends with identify can get messages. See Contact consent.
Does push work in Expo Go?
Events work in Expo Go. Remote push does not work in Expo Go on Android from Expo SDK 53. Use a development build.