Build Mobile Apps with AI

Native Device Features

5 min read

Goal: Integrate camera, location services, push notifications, and secure storage into your mobile app.

Understanding Expo Modules

Expo provides two types of modules:

  1. Expo Go Compatible - Work immediately in Expo Go app
  2. 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

  1. Permission handling differs between iOS and Android - always check and request gracefully
  2. Development builds are required for advanced native features
  3. Expo modules provide consistent APIs across platforms
  4. Secure Store should be used for sensitive data, not AsyncStorage
  5. Background tasks need special configuration in app.json

ميزات الجهاز الأصلية

الهدف: دمج الكاميرا وخدمات الموقع والإشعارات والتخزين الآمن في تطبيق الموبايل.

فهم وحدات Expo

Expo يوفر نوعين من الوحدات:

  1. متوافقة مع Expo Go - تعمل فوراً في تطبيق Expo Go
  2. تتطلب 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,
  };
}

النقاط الرئيسية

  1. التعامل مع الأذونات يختلف بين iOS وAndroid - تحقق واطلب بأناقة دائماً
  2. Development builds مطلوبة للميزات الأصلية المتقدمة
  3. وحدات Expo توفر APIs متسقة عبر المنصات
  4. Secure Store يجب استخدامه للبيانات الحساسة، وليس AsyncStorage
  5. المهام الخلفية تحتاج تكويناً خاصاً في app.json

Quiz

Module 2: Build Mobile Apps with AI

Take Quiz