بناء تطبيقات الموبايل مع AI
ميزات الجهاز الأصلية
5 دقيقة للقراءة
Goal: Integrate camera, location services, push notifications, and secure storage into your mobile app.
Understanding Expo Modules
Expo provides two types of modules:
- Expo Go Compatible - Work immediately in Expo Go app
- Development Build Required - Need custom native code
| Feature | Expo Go | Dev Build |
|---|---|---|
| Camera (basic) | ✅ | ✅ |
| Location | ✅ | ✅ |
| Notifications (local) | ✅ | ✅ |
| Push Notifications | ❌ | ✅ |
| Secure Store | ✅ | ✅ |
| In-App Purchases | ❌ | ✅ |
| Background Location | ❌ | ✅ |
Camera Integration
AI Prompt for Camera Feature
Add camera functionality to my Expo app:
## Requirements
- Photo capture with front/back camera toggle
- Preview captured image before saving
- Save to device gallery
- Optional: barcode/QR code scanning
## Tech Stack
- expo-camera
- expo-image-picker (for gallery access)
- expo-media-library (for saving)
## UI Needs
- Full-screen camera preview
- Capture button with animation
- Flash toggle
- Camera flip button
- Gallery access button
Camera Implementation
// hooks/use-camera.ts
import { useState, useRef } from 'react';
import { CameraView, useCameraPermissions } from 'expo-camera';
import * as MediaLibrary from 'expo-media-library';
export function useCamera() {
const [facing, setFacing] = useState<'front' | 'back'>('back');
const [flash, setFlash] = useState<'off' | 'on'>('off');
const [photo, setPhoto] = useState<string | null>(null);
const cameraRef = useRef<CameraView>(null);
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
const [mediaPermission, requestMediaPermission] =
MediaLibrary.usePermissions();
const toggleFacing = () => {
setFacing((current) => (current === 'back' ? 'front' : 'back'));
};
const toggleFlash = () => {
setFlash((current) => (current === 'off' ? 'on' : 'off'));
};
const takePicture = async () => {
if (!cameraRef.current) return;
const result = await cameraRef.current.takePictureAsync({
quality: 0.8,
base64: false,
});
if (result?.uri) {
setPhoto(result.uri);
}
};
const savePicture = async () => {
if (!photo) return;
if (!mediaPermission?.granted) {
const permission = await requestMediaPermission();
if (!permission.granted) return;
}
await MediaLibrary.saveToLibraryAsync(photo);
setPhoto(null);
};
const discardPicture = () => {
setPhoto(null);
};
return {
cameraRef,
facing,
flash,
photo,
cameraPermission,
requestCameraPermission,
toggleFacing,
toggleFlash,
takePicture,
savePicture,
discardPicture,
};
}
// components/camera-screen.tsx
import { View, Image, Pressable, Text } from 'react-native';
import { CameraView } from 'expo-camera';
import { useCamera } from '@/hooks/use-camera';
import {
Camera,
SwitchCamera,
Zap,
ZapOff,
Check,
X
} from 'lucide-react-native';
export function CameraScreen() {
const {
cameraRef,
facing,
flash,
photo,
cameraPermission,
requestCameraPermission,
toggleFacing,
toggleFlash,
takePicture,
savePicture,
discardPicture,
} = useCamera();
if (!cameraPermission) {
return <View className="flex-1 bg-black" />;
}
if (!cameraPermission.granted) {
return (
<View className="flex-1 items-center justify-center bg-black p-8">
<Text className="mb-4 text-center text-white">
Camera access is required to take photos
</Text>
<Pressable
onPress={requestCameraPermission}
className="rounded-lg bg-primary px-6 py-3"
>
<Text className="font-semibold text-white">
Grant Permission
</Text>
</Pressable>
</View>
);
}
if (photo) {
return (
<View className="flex-1 bg-black">
<Image source={{ uri: photo }} className="flex-1" />
<View className="absolute bottom-12 left-0 right-0 flex-row justify-center gap-8">
<Pressable
onPress={discardPicture}
className="h-16 w-16 items-center justify-center rounded-full bg-red-500"
>
<X size={32} color="white" />
</Pressable>
<Pressable
onPress={savePicture}
className="h-16 w-16 items-center justify-center rounded-full bg-green-500"
>
<Check size={32} color="white" />
</Pressable>
</View>
</View>
);
}
return (
<View className="flex-1 bg-black">
<CameraView
ref={cameraRef}
facing={facing}
flash={flash}
className="flex-1"
>
{/* Top controls */}
<View className="flex-row justify-between p-4 pt-12">
<Pressable
onPress={toggleFlash}
className="h-12 w-12 items-center justify-center rounded-full bg-black/50"
>
{flash === 'on' ? (
<Zap size={24} color="yellow" />
) : (
<ZapOff size={24} color="white" />
)}
</Pressable>
</View>
{/* Bottom controls */}
<View className="absolute bottom-12 left-0 right-0 flex-row items-center justify-center gap-8">
<Pressable
onPress={toggleFacing}
className="h-14 w-14 items-center justify-center rounded-full bg-white/20"
>
<SwitchCamera size={28} color="white" />
</Pressable>
<Pressable
onPress={takePicture}
className="h-20 w-20 items-center justify-center rounded-full border-4 border-white bg-white/20"
>
<Camera size={36} color="white" />
</Pressable>
<View className="h-14 w-14" />
</View>
</CameraView>
</View>
);
}
Location Services
AI Prompt for Location Feature
Implement location tracking in my Expo app:
## Requirements
- Get user's current location
- Show location on a map
- Track location changes in real-time
- Geocode addresses to coordinates
- Reverse geocode coordinates to addresses
## Considerations
- Battery optimization (accuracy vs power)
- Background location (if needed)
- Permission handling for iOS and Android
- Graceful fallback when location unavailable
Location Implementation
// hooks/use-location.ts
import { useState, useEffect } from 'react';
import * as Location from 'expo-location';
interface LocationData {
latitude: number;
longitude: number;
accuracy: number | null;
altitude: number | null;
heading: number | null;
speed: number | null;
}
interface Address {
street: string | null;
city: string | null;
region: string | null;
country: string | null;
postalCode: string | null;
}
export function useLocation() {
const [location, setLocation] = useState<LocationData | null>(null);
const [address, setAddress] = useState<Address | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const requestPermission = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
return status === 'granted';
};
const getCurrentLocation = async () => {
setIsLoading(true);
setError(null);
try {
const hasPermission = await requestPermission();
if (!hasPermission) {
setError('Location permission denied');
return;
}
const position = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
const locationData: LocationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
};
setLocation(locationData);
// Reverse geocode to get address
const [geocoded] = await Location.reverseGeocodeAsync({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
if (geocoded) {
setAddress({
street: geocoded.street,
city: geocoded.city,
region: geocoded.region,
country: geocoded.country,
postalCode: geocoded.postalCode,
});
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to get location');
} finally {
setIsLoading(false);
}
};
const watchLocation = async (
callback: (location: LocationData) => void
) => {
const hasPermission = await requestPermission();
if (!hasPermission) return null;
return Location.watchPositionAsync(
{
accuracy: Location.Accuracy.Balanced,
timeInterval: 5000,
distanceInterval: 10,
},
(position) => {
const locationData: LocationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
};
callback(locationData);
}
);
};
const geocodeAddress = async (address: string) => {
const results = await Location.geocodeAsync(address);
return results[0] || null;
};
return {
location,
address,
isLoading,
error,
getCurrentLocation,
watchLocation,
geocodeAddress,
};
}
// components/location-card.tsx
import { View, Text, Pressable, ActivityIndicator } from 'react-native';
import { useLocation } from '@/hooks/use-location';
import { MapPin, Navigation, RefreshCw } from 'lucide-react-native';
export function LocationCard() {
const {
location,
address,
isLoading,
error,
getCurrentLocation,
} = useLocation();
return (
<View className="rounded-xl bg-card p-4">
<View className="mb-4 flex-row items-center gap-2">
<MapPin size={20} className="text-primary" />
<Text className="text-lg font-semibold text-foreground">
Your Location
</Text>
</View>
{error && (
<Text className="mb-4 text-red-500">{error}</Text>
)}
{location ? (
<View className="gap-2">
<View className="flex-row items-center gap-2">
<Navigation size={16} className="text-muted-foreground" />
<Text className="text-muted-foreground">
{location.latitude.toFixed(6)}, {location.longitude.toFixed(6)}
</Text>
</View>
{address && (
<Text className="text-foreground">
{[address.street, address.city, address.region]
.filter(Boolean)
.join(', ')}
</Text>
)}
{location.accuracy && (
<Text className="text-sm text-muted-foreground">
Accuracy: ±{Math.round(location.accuracy)}m
</Text>
)}
</View>
) : (
<Text className="text-muted-foreground">
Tap refresh to get your location
</Text>
)}
<Pressable
onPress={getCurrentLocation}
disabled={isLoading}
className="mt-4 flex-row items-center justify-center gap-2 rounded-lg bg-primary py-3"
>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<>
<RefreshCw size={18} color="white" />
<Text className="font-semibold text-white">
{location ? 'Update' : 'Get Location'}
</Text>
</>
)}
</Pressable>
</View>
);
}
Push Notifications
AI Prompt for Notifications
Set up push notifications in my Expo app:
## Requirements
- Local notifications (reminders, alerts)
- Push notifications via Expo Push Service
- Handle notification taps to navigate to specific screens
- Notification categories with actions
- Badge management
## Platform Considerations
- iOS requires explicit permission
- Android notification channels (API 26+)
- Handle foreground vs background notifications
Notifications Implementation
// lib/notifications.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import Constants from 'expo-constants';
// Configure notification behavior
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export 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 permission if not granted
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 projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = await Notifications.getExpoPushTokenAsync({
projectId,
});
// Android requires notification channel
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#6366f1',
});
}
return token.data;
}
export async function scheduleLocalNotification({
title,
body,
data,
trigger,
}: {
title: string;
body: string;
data?: Record<string, unknown>;
trigger?: Notifications.NotificationTriggerInput;
}) {
return Notifications.scheduleNotificationAsync({
content: {
title,
body,
data,
sound: true,
},
trigger: trigger ?? null, // null = immediate
});
}
export async function cancelAllNotifications() {
await Notifications.cancelAllScheduledNotificationsAsync();
}
export async function setBadgeCount(count: number) {
await Notifications.setBadgeCountAsync(count);
}
// hooks/use-notifications.ts
import { useEffect, useRef, useState } from 'react';
import * as Notifications from 'expo-notifications';
import { useRouter } from 'expo-router';
import { registerForPushNotifications } from '@/lib/notifications';
export function useNotifications() {
const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
const [notification, setNotification] =
useState<Notifications.Notification | null>(null);
const notificationListener = useRef<Notifications.EventSubscription>();
const responseListener = useRef<Notifications.EventSubscription>();
const router = useRouter();
useEffect(() => {
// Register for push notifications
registerForPushNotifications().then((token) => {
if (token) {
setExpoPushToken(token);
// Send token to your backend
// api.post('/users/push-token', { token });
}
});
// Listen for incoming notifications
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification);
});
// Listen for notification taps
responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data;
// Navigate based on notification data
if (data?.screen) {
router.push(data.screen as string);
}
});
return () => {
if (notificationListener.current) {
Notifications.removeNotificationSubscription(
notificationListener.current
);
}
if (responseListener.current) {
Notifications.removeNotificationSubscription(
responseListener.current
);
}
};
}, [router]);
return {
expoPushToken,
notification,
};
}
Secure Storage
For sensitive data like tokens and credentials:
// lib/secure-storage.ts
import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'auth_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
export const secureStorage = {
async getToken(): Promise<string | null> {
return SecureStore.getItemAsync(TOKEN_KEY);
},
async setToken(token: string): Promise<void> {
await SecureStore.setItemAsync(TOKEN_KEY, token);
},
async getRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
},
async setRefreshToken(token: string): Promise<void> {
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, token);
},
async clearTokens(): Promise<void> {
await SecureStore.deleteItemAsync(TOKEN_KEY);
await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
},
async setItem(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value);
},
async getItem(key: string): Promise<string | null> {
return SecureStore.getItemAsync(key);
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
};
Biometric Authentication
// hooks/use-biometrics.ts
import { useState, useEffect } from 'react';
import * as LocalAuthentication from 'expo-local-authentication';
type BiometricType = 'fingerprint' | 'facial' | 'iris' | 'none';
export function useBiometrics() {
const [isAvailable, setIsAvailable] = useState(false);
const [biometricType, setBiometricType] = useState<BiometricType>('none');
useEffect(() => {
checkBiometrics();
}, []);
const checkBiometrics = async () => {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setIsAvailable(compatible && enrolled);
if (compatible) {
const types =
await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types.includes(
LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
)) {
setBiometricType('facial');
} else if (types.includes(
LocalAuthentication.AuthenticationType.FINGERPRINT
)) {
setBiometricType('fingerprint');
} else if (types.includes(
LocalAuthentication.AuthenticationType.IRIS
)) {
setBiometricType('iris');
}
}
};
const authenticate = async (
promptMessage = 'Authenticate to continue'
): Promise<boolean> => {
if (!isAvailable) return false;
const result = await LocalAuthentication.authenticateAsync({
promptMessage,
fallbackLabel: 'Use passcode',
cancelLabel: 'Cancel',
disableDeviceFallback: false,
});
return result.success;
};
return {
isAvailable,
biometricType,
authenticate,
};
}
Key Takeaways
- Permission handling differs between iOS and Android - always check and request gracefully
- Development builds are required for advanced native features
- Expo modules provide consistent APIs across platforms
- Secure Store should be used for sensitive data, not AsyncStorage
- Background tasks need special configuration in app.json
ميزات الجهاز الأصلية
الهدف: دمج الكاميرا وخدمات الموقع والإشعارات والتخزين الآمن في تطبيق الموبايل.
فهم وحدات Expo
Expo يوفر نوعين من الوحدات:
- متوافقة مع Expo Go - تعمل فوراً في تطبيق Expo Go
- تتطلب Development Build - تحتاج كود native مخصص
| الميزة | Expo Go | Dev Build |
|---|---|---|
| الكاميرا (أساسي) | ✅ | ✅ |
| الموقع | ✅ | ✅ |
| الإشعارات (محلية) | ✅ | ✅ |
| إشعارات Push | ❌ | ✅ |
| التخزين الآمن | ✅ | ✅ |
| المشتريات داخل التطبيق | ❌ | ✅ |
| الموقع في الخلفية | ❌ | ✅ |
دمج الكاميرا
برومبت AI لميزة الكاميرا
أضف وظيفة الكاميرا لتطبيق Expo:
## المتطلبات
- التقاط الصور مع تبديل الكاميرا الأمامية/الخلفية
- معاينة الصورة الملتقطة قبل الحفظ
- الحفظ في معرض الجهاز
- اختياري: مسح الباركود/رمز QR
## التقنيات
- expo-camera
- expo-image-picker (للوصول للمعرض)
- expo-media-library (للحفظ)
## احتياجات الواجهة
- معاينة كاميرا بملء الشاشة
- زر التقاط مع رسوم متحركة
- تبديل الفلاش
- زر قلب الكاميرا
- زر الوصول للمعرض
تطبيق الكاميرا
// hooks/use-camera.ts
import { useState, useRef } from 'react';
import { CameraView, useCameraPermissions } from 'expo-camera';
import * as MediaLibrary from 'expo-media-library';
export function useCamera() {
const [facing, setFacing] = useState<'front' | 'back'>('back');
const [flash, setFlash] = useState<'off' | 'on'>('off');
const [photo, setPhoto] = useState<string | null>(null);
const cameraRef = useRef<CameraView>(null);
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
const [mediaPermission, requestMediaPermission] =
MediaLibrary.usePermissions();
const toggleFacing = () => {
setFacing((current) => (current === 'back' ? 'front' : 'back'));
};
const toggleFlash = () => {
setFlash((current) => (current === 'off' ? 'on' : 'off'));
};
const takePicture = async () => {
if (!cameraRef.current) return;
const result = await cameraRef.current.takePictureAsync({
quality: 0.8,
base64: false,
});
if (result?.uri) {
setPhoto(result.uri);
}
};
const savePicture = async () => {
if (!photo) return;
if (!mediaPermission?.granted) {
const permission = await requestMediaPermission();
if (!permission.granted) return;
}
await MediaLibrary.saveToLibraryAsync(photo);
setPhoto(null);
};
const discardPicture = () => {
setPhoto(null);
};
return {
cameraRef,
facing,
flash,
photo,
cameraPermission,
requestCameraPermission,
toggleFacing,
toggleFlash,
takePicture,
savePicture,
discardPicture,
};
}
// components/camera-screen.tsx
import { View, Image, Pressable, Text } from 'react-native';
import { CameraView } from 'expo-camera';
import { useCamera } from '@/hooks/use-camera';
import {
Camera,
SwitchCamera,
Zap,
ZapOff,
Check,
X
} from 'lucide-react-native';
export function CameraScreen() {
const {
cameraRef,
facing,
flash,
photo,
cameraPermission,
requestCameraPermission,
toggleFacing,
toggleFlash,
takePicture,
savePicture,
discardPicture,
} = useCamera();
if (!cameraPermission) {
return <View className="flex-1 bg-black" />;
}
if (!cameraPermission.granted) {
return (
<View className="flex-1 items-center justify-center bg-black p-8">
<Text className="mb-4 text-center text-white">
الوصول للكاميرا مطلوب لالتقاط الصور
</Text>
<Pressable
onPress={requestCameraPermission}
className="rounded-lg bg-primary px-6 py-3"
>
<Text className="font-semibold text-white">
منح الإذن
</Text>
</Pressable>
</View>
);
}
if (photo) {
return (
<View className="flex-1 bg-black">
<Image source={{ uri: photo }} className="flex-1" />
<View className="absolute bottom-12 left-0 right-0 flex-row justify-center gap-8">
<Pressable
onPress={discardPicture}
className="h-16 w-16 items-center justify-center rounded-full bg-red-500"
>
<X size={32} color="white" />
</Pressable>
<Pressable
onPress={savePicture}
className="h-16 w-16 items-center justify-center rounded-full bg-green-500"
>
<Check size={32} color="white" />
</Pressable>
</View>
</View>
);
}
return (
<View className="flex-1 bg-black">
<CameraView
ref={cameraRef}
facing={facing}
flash={flash}
className="flex-1"
>
{/* أزرار التحكم العلوية */}
<View className="flex-row justify-between p-4 pt-12">
<Pressable
onPress={toggleFlash}
className="h-12 w-12 items-center justify-center rounded-full bg-black/50"
>
{flash === 'on' ? (
<Zap size={24} color="yellow" />
) : (
<ZapOff size={24} color="white" />
)}
</Pressable>
</View>
{/* أزرار التحكم السفلية */}
<View className="absolute bottom-12 left-0 right-0 flex-row items-center justify-center gap-8">
<Pressable
onPress={toggleFacing}
className="h-14 w-14 items-center justify-center rounded-full bg-white/20"
>
<SwitchCamera size={28} color="white" />
</Pressable>
<Pressable
onPress={takePicture}
className="h-20 w-20 items-center justify-center rounded-full border-4 border-white bg-white/20"
>
<Camera size={36} color="white" />
</Pressable>
<View className="h-14 w-14" />
</View>
</CameraView>
</View>
);
}
خدمات الموقع
برومبت AI لميزة الموقع
طبّق تتبع الموقع في تطبيق Expo:
## المتطلبات
- الحصول على موقع المستخدم الحالي
- عرض الموقع على خريطة
- تتبع تغييرات الموقع في الوقت الفعلي
- تحويل العناوين إلى إحداثيات
- تحويل الإحداثيات إلى عناوين
## الاعتبارات
- تحسين البطارية (الدقة مقابل الطاقة)
- الموقع في الخلفية (إذا لزم)
- التعامل مع الأذونات لـ iOS وAndroid
- بديل سلس عند عدم توفر الموقع
تطبيق الموقع
// hooks/use-location.ts
import { useState, useEffect } from 'react';
import * as Location from 'expo-location';
interface LocationData {
latitude: number;
longitude: number;
accuracy: number | null;
altitude: number | null;
heading: number | null;
speed: number | null;
}
interface Address {
street: string | null;
city: string | null;
region: string | null;
country: string | null;
postalCode: string | null;
}
export function useLocation() {
const [location, setLocation] = useState<LocationData | null>(null);
const [address, setAddress] = useState<Address | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const requestPermission = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
return status === 'granted';
};
const getCurrentLocation = async () => {
setIsLoading(true);
setError(null);
try {
const hasPermission = await requestPermission();
if (!hasPermission) {
setError('إذن الموقع مرفوض');
return;
}
const position = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
const locationData: LocationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
};
setLocation(locationData);
// تحويل عكسي للحصول على العنوان
const [geocoded] = await Location.reverseGeocodeAsync({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
if (geocoded) {
setAddress({
street: geocoded.street,
city: geocoded.city,
region: geocoded.region,
country: geocoded.country,
postalCode: geocoded.postalCode,
});
}
} catch (err) {
setError(err instanceof Error ? err.message : 'فشل في الحصول على الموقع');
} finally {
setIsLoading(false);
}
};
const watchLocation = async (
callback: (location: LocationData) => void
) => {
const hasPermission = await requestPermission();
if (!hasPermission) return null;
return Location.watchPositionAsync(
{
accuracy: Location.Accuracy.Balanced,
timeInterval: 5000,
distanceInterval: 10,
},
(position) => {
const locationData: LocationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
};
callback(locationData);
}
);
};
const geocodeAddress = async (address: string) => {
const results = await Location.geocodeAsync(address);
return results[0] || null;
};
return {
location,
address,
isLoading,
error,
getCurrentLocation,
watchLocation,
geocodeAddress,
};
}
إشعارات Push
برومبت AI للإشعارات
إعداد إشعارات push في تطبيق Expo:
## المتطلبات
- إشعارات محلية (تذكيرات، تنبيهات)
- إشعارات push عبر خدمة Expo Push
- التعامل مع النقر على الإشعارات للتنقل لشاشات محددة
- فئات إشعارات مع إجراءات
- إدارة الشارات
## اعتبارات المنصة
- iOS يتطلب إذناً صريحاً
- قنوات إشعارات Android (API 26+)
- التعامل مع الإشعارات في المقدمة مقابل الخلفية
تطبيق الإشعارات
// lib/notifications.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import Constants from 'expo-constants';
// تكوين سلوك الإشعارات
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export async function registerForPushNotifications() {
if (!Device.isDevice) {
console.log('إشعارات Push تتطلب جهازاً فعلياً');
return null;
}
// التحقق من الإذن الحالي
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
// طلب الإذن إذا لم يُمنح
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('إذن إشعارات push مرفوض');
return null;
}
// الحصول على رمز Expo push
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = await Notifications.getExpoPushTokenAsync({
projectId,
});
// Android يتطلب قناة إشعارات
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#6366f1',
});
}
return token.data;
}
export async function scheduleLocalNotification({
title,
body,
data,
trigger,
}: {
title: string;
body: string;
data?: Record<string, unknown>;
trigger?: Notifications.NotificationTriggerInput;
}) {
return Notifications.scheduleNotificationAsync({
content: {
title,
body,
data,
sound: true,
},
trigger: trigger ?? null, // null = فوري
});
}
export async function cancelAllNotifications() {
await Notifications.cancelAllScheduledNotificationsAsync();
}
export async function setBadgeCount(count: number) {
await Notifications.setBadgeCountAsync(count);
}
التخزين الآمن
للبيانات الحساسة مثل الرموز وبيانات الاعتماد:
// lib/secure-storage.ts
import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'auth_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
export const secureStorage = {
async getToken(): Promise<string | null> {
return SecureStore.getItemAsync(TOKEN_KEY);
},
async setToken(token: string): Promise<void> {
await SecureStore.setItemAsync(TOKEN_KEY, token);
},
async getRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
},
async setRefreshToken(token: string): Promise<void> {
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, token);
},
async clearTokens(): Promise<void> {
await SecureStore.deleteItemAsync(TOKEN_KEY);
await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
},
async setItem(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value);
},
async getItem(key: string): Promise<string | null> {
return SecureStore.getItemAsync(key);
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
};
المصادقة البيومترية
// hooks/use-biometrics.ts
import { useState, useEffect } from 'react';
import * as LocalAuthentication from 'expo-local-authentication';
type BiometricType = 'fingerprint' | 'facial' | 'iris' | 'none';
export function useBiometrics() {
const [isAvailable, setIsAvailable] = useState(false);
const [biometricType, setBiometricType] = useState<BiometricType>('none');
useEffect(() => {
checkBiometrics();
}, []);
const checkBiometrics = async () => {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setIsAvailable(compatible && enrolled);
if (compatible) {
const types =
await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types.includes(
LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION
)) {
setBiometricType('facial');
} else if (types.includes(
LocalAuthentication.AuthenticationType.FINGERPRINT
)) {
setBiometricType('fingerprint');
} else if (types.includes(
LocalAuthentication.AuthenticationType.IRIS
)) {
setBiometricType('iris');
}
}
};
const authenticate = async (
promptMessage = 'المصادقة للمتابعة'
): Promise<boolean> => {
if (!isAvailable) return false;
const result = await LocalAuthentication.authenticateAsync({
promptMessage,
fallbackLabel: 'استخدم رمز المرور',
cancelLabel: 'إلغاء',
disableDeviceFallback: false,
});
return result.success;
};
return {
isAvailable,
biometricType,
authenticate,
};
}
النقاط الرئيسية
- التعامل مع الأذونات يختلف بين iOS وAndroid - تحقق واطلب بأناقة دائماً
- Development builds مطلوبة للميزات الأصلية المتقدمة
- وحدات Expo توفر APIs متسقة عبر المنصات
- Secure Store يجب استخدامه للبيانات الحساسة، وليس AsyncStorage
- المهام الخلفية تحتاج تكويناً خاصاً في app.json