Registering for push
Permissions + token capture.
Push notifications go: phone gets a token from Apple / Google → mobile sends the token to your API → API saves it on the user → later, a job worker sends pushes to that token via Expo Push. This lesson covers the first two steps.
The plumbing
$pnpm add expo-notifications expo-device
Already in the Grit mobile scaffold.
Request permission + grab the token
import * as Notifications from 'expo-notifications'import * as Device from 'expo-device'import Constants from 'expo-constants'import { Platform } from 'react-native'export async function registerForPush(): Promise<string | null> {if (!Device.isDevice) return null // simulator can't receive real pushes// iOS-specific: must request permissionconst { status: existing } = await Notifications.getPermissionsAsync()let final = existingif (existing !== 'granted') {const { status } = await Notifications.requestPermissionsAsync()final = status}if (final !== 'granted') return null // user denied// Android-specific: must create a channelif (Platform.OS === 'android') {await Notifications.setNotificationChannelAsync('default', {name: 'default',importance: Notifications.AndroidImportance.DEFAULT,})}const projectId = Constants.expoConfig?.extra?.eas?.projectIdconst token = (await Notifications.getExpoPushTokenAsync({ projectId })).datareturn token // ExponentPushToken[xxxxxxxxxxxxxxxxxxxx]}
Returns an Expo Push token — a string starting with ExponentPushToken[. Expo's push service uses it to identify the device when you want to send a push.
Save it to your API
async function login(email: string, password: string) {// ... existing login logic ...// After successful login, register for push and save the tokenconst pushToken = await registerForPush()if (pushToken) {await api.post('/api/users/me/push-token', { push_token: pushToken })}}
On the API side, add a handler that updates the user's push_token column.
Foreground vs background — what the user sees
- App in foreground — Expo doesn't show a system notification by default. Use
Notifications.setNotificationHandlerto decide whether to show a banner. - App backgrounded / killed — system shows the notification automatically. Tapping it deep-links into the app.
Notifications.setNotificationHandler({handleNotification: async () => ({shouldShowAlert: true,shouldPlaySound: true,shouldSetBadge: true,}),})
Device.isDevice is false in the iOS simulator and Android emulator, so push registration is skipped. You must test push on a physical device.Handling taps + deep-links
Notifications.addNotificationResponseReceivedListener((response) => {const data = response.notification.request.content.dataif (data?.orderId) {router.push(`/orders/${data.orderId}`)}})
The data payload you sent from the API arrives here. Use it to navigate deep into the app — "new message" opens the thread, "order shipped" opens the order.
Updating the token over time
Expo push tokens can change — after app reinstall, after iOS re-permission cycle. Re-register on every cold start so your API always has the current one:
useEffect(() => {if (isAuthenticated) {registerForPush().then((token) => {if (token) api.post('/api/users/me/push-token', { push_token: token })})}}, [isAuthenticated])
Quick check
Try it
Register your device for push:
- On a physical phone (not the simulator), run the app via Expo Go.
- Sign in.
- Call
registerForPush()on mount (or log in). Console-log the returned token. - The token starts with
ExponentPushToken[— copy it. - Paste the token in
notes.md.
What's next
Last lesson of the chapter — actually send a push from your Grit API to your phone.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub