Marketplace rn-native-features
Native iOS features in Expo React Native apps. Use when implementing camera, push notifications, haptics, permissions, device sensors, or other native APIs in Expo.
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cjharmath/rn-native-features" ~/.claude/skills/aiskillstore-marketplace-rn-native-features && rm -rf "$T"
manifest:
skills/cjharmath/rn-native-features/SKILL.mdsource content
Native Features (Expo)
Permissions Pattern
Always request permissions before using native features:
import * as Camera from 'expo-camera'; async function requestCameraPermission() { const { status } = await Camera.requestCameraPermissionsAsync(); if (status !== 'granted') { // Guide user to settings if denied Alert.alert( 'Camera Access Required', 'Please enable camera access in Settings to take photos.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Open Settings', onPress: () => Linking.openSettings() }, ] ); return false; } return true; }
Check permission status first
import * as Camera from 'expo-camera'; function usePermission() { const [permission, requestPermission] = Camera.useCameraPermissions(); // permission.granted - boolean // permission.canAskAgain - false if user selected "Don't ask again" // permission.status - 'granted' | 'denied' | 'undetermined' return { permission, requestPermission }; }
Camera
Basic Camera with Photo Capture
import { CameraView, useCameraPermissions } from 'expo-camera'; import { useRef, useState } from 'react'; export function CameraScreen() { const [permission, requestPermission] = useCameraPermissions(); const [facing, setFacing] = useState<'front' | 'back'>('back'); const cameraRef = useRef<CameraView>(null); if (!permission) return <View />; if (!permission.granted) { return ( <View> <Text>Camera access is required</Text> <Button title="Grant Permission" onPress={requestPermission} /> </View> ); } const takePicture = async () => { if (cameraRef.current) { const photo = await cameraRef.current.takePictureAsync({ quality: 0.8, base64: false, exif: false, }); console.log('Photo URI:', photo.uri); // photo.uri is a local file path } }; return ( <View style={{ flex: 1 }}> <CameraView ref={cameraRef} style={{ flex: 1 }} facing={facing} > <View style={styles.controls}> <Button title="Flip" onPress={() => setFacing(f => f === 'back' ? 'front' : 'back')} /> <Button title="Take Photo" onPress={takePicture} /> </View> </CameraView> </View> ); }
Image Picker (Gallery + Camera)
Often simpler than full camera UI:
import * as ImagePicker from 'expo-image-picker'; async function pickImage() { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled) { return result.assets[0].uri; } } async function takePhoto() { const permission = await ImagePicker.requestCameraPermissionsAsync(); if (!permission.granted) return; const result = await ImagePicker.launchCameraAsync({ allowsEditing: true, aspect: [1, 1], quality: 0.8, }); if (!result.canceled) { return result.assets[0].uri; } }
Push Notifications
Setup with expo-notifications
import * as Notifications from 'expo-notifications'; import * as Device from 'expo-device'; import { Platform } from 'react-native'; // Configure how notifications appear when app is foregrounded Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, }), }); async function registerForPushNotifications() { if (!Device.isDevice) { console.log('Push notifications require a physical device'); return null; } // Check existing permission const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; // Request if not determined if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } if (finalStatus !== 'granted') { console.log('Push notification permission denied'); return null; } // Get Expo push token const token = await Notifications.getExpoPushTokenAsync({ projectId: 'your-expo-project-id', // From app.json }); return token.data; // "ExponentPushToken[xxxx]" }
Handle incoming notifications
import { useEffect, useRef } from 'react'; import * as Notifications from 'expo-notifications'; export function useNotificationHandler() { const notificationListener = useRef<Notifications.Subscription>(); const responseListener = useRef<Notifications.Subscription>(); useEffect(() => { // Notification received while app is foregrounded notificationListener.current = Notifications.addNotificationReceivedListener( (notification) => { console.log('Received:', notification.request.content); } ); // User tapped on notification responseListener.current = Notifications.addNotificationResponseReceivedListener( (response) => { const data = response.notification.request.content.data; // Navigate based on notification data if (data.screen) { router.push(data.screen); } } ); return () => { notificationListener.current?.remove(); responseListener.current?.remove(); }; }, []); }
Send test notification locally
async function sendTestNotification() { await Notifications.scheduleNotificationAsync({ content: { title: "Match Reminder", body: "Your match starts in 30 minutes!", data: { screen: '/match/123' }, }, trigger: { seconds: 5 }, }); }
Send from backend (Expo Push API)
# FastAPI example import httpx async def send_push_notification( expo_push_token: str, title: str, body: str, data: dict = None ): message = { "to": expo_push_token, "title": title, "body": body, "data": data or {}, } async with httpx.AsyncClient() as client: response = await client.post( "https://exp.host/--/api/v2/push/send", json=message, headers={"Content-Type": "application/json"}, ) return response.json()
Haptic Feedback
import * as Haptics from 'expo-haptics'; // Light tap - for UI interactions Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); // Medium - for confirmations Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); // Heavy - for significant actions Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); // Success/Error/Warning - semantic feedback Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); // Selection - for picker/scroll Haptics.selectionAsync();
Good haptic patterns
// Button press <Pressable onPress={() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); handlePress(); }} /> // Form submission success await submitForm(); Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); // Error state if (error) { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error); } // Picker/Scroll selection <Picker onValueChange={(value) => { Haptics.selectionAsync(); setValue(value); }} />
Location
import * as Location from 'expo-location'; async function getCurrentLocation() { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { return null; } const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced, }); return { latitude: location.coords.latitude, longitude: location.coords.longitude, }; } // Watch location changes function useLocationTracking() { const [location, setLocation] = useState(null); useEffect(() => { let subscription: Location.LocationSubscription; (async () => { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') return; subscription = await Location.watchPositionAsync( { accuracy: Location.Accuracy.High, distanceInterval: 10, // meters }, (loc) => setLocation(loc.coords) ); })(); return () => subscription?.remove(); }, []); return location; }
Local Storage
AsyncStorage (simple key-value)
import AsyncStorage from '@react-native-async-storage/async-storage'; // Store await AsyncStorage.setItem('user_preferences', JSON.stringify(prefs)); // Retrieve const prefs = JSON.parse(await AsyncStorage.getItem('user_preferences') || '{}'); // Remove await AsyncStorage.removeItem('user_preferences');
SecureStore (sensitive data)
import * as SecureStore from 'expo-secure-store'; // For tokens, credentials - encrypted storage await SecureStore.setItemAsync('auth_token', token); const token = await SecureStore.getItemAsync('auth_token'); await SecureStore.deleteItemAsync('auth_token');
App State & Lifecycle
import { useEffect } from 'react'; import { AppState, AppStateStatus } from 'react-native'; function useAppState(callback: (state: AppStateStatus) => void) { useEffect(() => { const subscription = AppState.addEventListener('change', callback); return () => subscription.remove(); }, [callback]); } // Usage useAppState((state) => { if (state === 'active') { // App came to foreground - refresh data refreshData(); } else if (state === 'background') { // App going to background - save state saveState(); } });
Keyboard Handling
import { KeyboardAvoidingView, Platform } from 'react-native'; // Wrap forms to avoid keyboard <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }} > <YourForm /> </KeyboardAvoidingView> // Dismiss keyboard import { Keyboard } from 'react-native'; Keyboard.dismiss(); // Listen to keyboard events import { useEffect } from 'react'; import { Keyboard } from 'react-native'; function useKeyboardVisible() { const [visible, setVisible] = useState(false); useEffect(() => { const showSub = Keyboard.addListener('keyboardDidShow', () => setVisible(true)); const hideSub = Keyboard.addListener('keyboardDidHide', () => setVisible(false)); return () => { showSub.remove(); hideSub.remove(); }; }, []); return visible; }
Common Issues
| Issue | Solution |
|---|---|
| Camera black screen | Check permissions, ensure CameraView has explicit dimensions |
| Notifications not received | Verify physical device, check push token registration |
| Location inaccurate | Use , check device location services enabled |
| Haptics not working | Only works on physical device, not simulator |
| SecureStore size limit | Max ~2KB per item, use for tokens not large data |