Accessing Mobile Hardware: Camera, GPS, and Push Notifications
If your service only needs to display text and buttons, a responsive mobile website would suffice.
The reason we go through the trouble of developing a "native app" is to access mobile hardware: camera, GPS location, push notifications, biometrics (Face ID/fingerprint), and more.
In traditional React Native development, integrating these hardware features required modifying the underlying iOS Info.plist and Android AndroidManifest.xml, which was painful and error-prone.
The good news is: Expo SDK handles all this dirty work for us!
In this lesson, we’ll learn three of the most commonly used hardware features.
1. The Golden Rule of Permissions
Before accessing any hardware, we must comply with Apple and Google’s policies: we must first obtain the user’s consent.
If you call a hardware API without permission, your app will crash immediately.
Most of Expo’s hardware modules provide a Hook like usePermissions() to handle this elegantly:
- Check the current permission status.
- If not yet asked, prompt the user with a system dialog.
- If the user denies, we must display a message explaining why we need this permission and guide them to enable it in system settings.
2. Hands-on 1: Launching the Camera and Picking Photos (Image Picker)
Allow users to take photos or select images from their gallery to upload a profile picture.
Install the package:
npx expo install expo-image-picker
Code implementation:
import { useState } from 'react';
import { Button, Image, View, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
export default function CameraScreen() {
const [image, setImage] = useState<string | null>(null);
// 1. Function to request camera permissions
const requestPermissionAndPick = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission Required', 'We need camera access to let you take photos!');
return;
}
// 2. Launch the camera
let result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, // Restrict to photos only (no videos)
allowsEditing: true, // Allow cropping after taking the photo
aspect: [4, 3], // Crop aspect ratio
quality: 0.8, // Compress image quality (0-1) to save bandwidth
});
if (!result.canceled) {
// 3. Save the photo URI to State and display it
setImage(result.assets[0].uri);
}
};
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Open Camera" onPress={requestPermissionAndPick} />
{image && (
<Image source={{ uri: image }} style={{ width: 300, height: 300, marginTop: 20, borderRadius: 10 }} />
)}
</View>
);
}
Tip: If you want users to pick photos from their gallery, simply replace launchCameraAsync with launchImageLibraryAsync!
3. Hands-on 2: Fetching GPS Location Data (Location)
To build map features like Uber or Foodpanda, the first step is knowing where the user is.
Install the package:
npx expo install expo-location
Code implementation:
import { useState, useEffect } from 'react';
import { Text, View, ActivityIndicator } from 'react-native';
import * as Location from 'expo-location';
export default function LocationScreen() {
const [location, setLocation] = useState<Location.LocationObject | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
useEffect(() => {
(async () => {
// 1. Request location permission (foreground usage)
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('Permission denied. Unable to provide the service.');
return;
}
// 2. Fetch current coordinates
let currentLocation = await Location.getCurrentPositionAsync({});
setLocation(currentLocation);
})();
}, []);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
{errorMsg ? (
<Text style={{ color: 'red' }}>{errorMsg}</Text>
) : location ? (
<>
<Text>Your Latitude: {location.coords.latitude}</Text>
<Text>Your Longitude: {location.coords.longitude}</Text>
</>
) : (
<ActivityIndicator size="large" color="#0000ff" />
)}
</View>
);
}
4. Hands-on 3: Push Notifications
Push notifications are the most powerful tool for re-engaging users (retargeting).
With Expo, we don’t need to struggle with Apple APNs or Google FCM directly. Instead, we can use the Expo Push Notifications service, which unifies the logic for both platforms!
Install the packages:
npx expo install expo-notifications expo-device
How it works:
- When the app launches for the first time, request push notification permissions.
- Obtain a unique
ExpoPushToken(this acts like the device’s address). - Store this token in your backend database (e.g., Supabase).
- When you want to send a push notification, your backend simply sends an HTTP request to Expo’s API server with this token, and Expo will deliver the notification to the user’s device!
import * as Notifications from 'expo-notifications';
// Configure notification behavior when the app is in the foreground
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true, // Shows a pop-up notification
shouldPlaySound: true, // Plays a sound
shouldSetBadge: false,
}),
});
// Trigger a local notification (no network required, suitable for alarms or reminders)
async function scheduleLocalNotification() {
await Notifications.scheduleNotificationAsync({
content: {
title: "You have a new message! 📬",
body: 'This is a test notification triggered by the Vibe Tutor tutorial app.',
data: { url: '/chat/123' }, // Can include hidden data for deep linking
},
trigger: { seconds: 5 }, // Triggers after 5 seconds
});
}
[!CAUTION] Push Notification Pitfalls
The iOS Simulator does not support receiving real remote push notifications! You must compile and install the app on a physical iPhone to test full push functionality.
Now your app has "eyes" (camera) and a "sense of direction" (GPS). In the next chapter, we’ll learn how to manage complex internal data flows and API integrations—State Management.
Push Notification Architecture
Push notifications require three components: provider (server), platform (APNs/FCM), and client (app).
Setup
# Install notifications module
npx expo install expo-notifications expo-device
Request Permission
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
async function registerForPushNotifications() {
if (!Device.isDevice) {
alert('Must use physical device for push notifications');
return;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token');
return;
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
console.log('Push token:', token);
// Send token to your backend
await api.post('/users/push-token', { token });
}
Handle Incoming Notifications
// Configure notification handler
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
// Listen for notifications while app is foregrounded
useEffect(() => {
const subscription = Notifications.addNotificationReceivedListener(
(notification) => {
console.log('Notification received:', notification);
}
);
const responseSubscription = Notifications.addNotificationResponseReceivedListener(
(response) => {
// User tapped notification
const data = response.notification.request.content.data;
navigation.navigate(data.screen);
}
);
return () => {
subscription.remove();
responseSubscription.remove();
};
}, []);
Send Notification from Server
// Server-side code (Next.js API route)
import { Expo } from 'expo-server-sdk';
const expo = new Expo();
export async function POST(request: Request) {
const { pushToken, title, body, data } = await request.json();
const message = {
to: pushToken,
sound: 'default',
title,
body,
data,
};
await expo.sendPushNotificationsAsync([message]);
return Response.json({ success: true });
}
Sensors
import { Accelerometer } from 'expo-sensors';
import { useState, useEffect } from 'react';
export default function ShakeDetector() {
const [{ x, y, z }, setData] = useState({ x: 0, y: 0, z: 0 });
useEffect(() => {
const subscription = Accelerometer.addListener((data) => {
setData(data);
// Detect shake: sudden acceleration change
const magnitude = Math.sqrt(data.x ** 2 + data.y ** 2 + data.z ** 2);
if (magnitude > 2.5) {
console.log('📱 Shake detected!');
}
});
Accelerometer.setUpdateInterval(100); // 100ms
return () => subscription.remove();
}, []);
return (
<View>
<Text>Accelerometer:</Text>
<Text>x: {x.toFixed(2)}</Text>
<Text>y: {y.toFixed(2)}</Text>
<Text>z: {z.toFixed(2)}</Text>
</View>
);
}
Summary
Expo modules give your app access to camera, GPS, notifications, and sensors through simple JavaScript APIs — no native code required.
Key takeaways:
- Camera:
expo-camerawith barcode scanning support | - Location:
expo-locationwith foreground/background permissions | - Push notifications:
expo-notificationswith Expo push service | - iOS push testing requires a physical device, not simulator |
- Sensors:
expo-sensorsfor accelerometer, gyroscope, magnetometer | - Always request permissions before accessing device features |
- Expo push tokens identify devices for notification delivery |
- Handle notification taps to navigate to relevant screens |
What's Next: State Management
The next chapter covers managing app state and API integrations.