Building Enterprise Mobile Apps with Expo and React Native

Enterprise mobile app development with Expo and React Native: EAS Build, push notifications, offline support, deep linking, authentication, and App Store submission.

E
ECOSIRE Research and Development Team
|19 mars 202610 min de lecture2.2k Mots|

Création d'applications mobiles d'entreprise avec Expo et React Native

Expo est passée d'un outil de prototypage rapide à une plate-forme d'entreprise légitime. Avec Expo SDK 52 et la nouvelle architecture (Fabric renderer + JSI), vous pouvez créer des applications mobiles multiplateformes qui correspondent aux performances et à la convivialité des applications natives, tout en partageant du code avec votre application Web React et en l'expédiant sur iOS et Android à partir d'une seule base de code TypeScript.

Ce guide couvre les modèles qui séparent un projet de loisir d'une application d'entreprise de production : configuration EAS Build, infrastructure de notification push, synchronisation des données hors ligne, authentification biométrique et flux de travail de soumission sur l'App Store.

Points clés à retenir

  • Expo Go est destiné aux démos ; EAS Build est destiné à la production : commencez avec EAS dès le premier jour
  • La nouvelle architecture (Fabric + JSI) est opt-in mais offre des gains de performances mesurables
  • expo-notifications nécessite une configuration minutieuse pour la gestion du premier plan et de l'arrière-plan
  • Le support hors ligne nécessite une base de données locale (WatermelonDB ou SQLite) avec une logique de synchronisation
  • L'authentification biométrique avec expo-local-authentication est constituée de quelques lignes de code
  • Les liens profonds nécessitent une configuration à la fois dans app.json et dans les URL de rappel de votre fournisseur d'authentification
  • Les alias de chemin TypeScript fonctionnent dans Expo via babel-plugin-module-resolver
  • Ne stockez jamais de données sensibles dans AsyncStorage — utilisez expo-secure-store pour les jetons

Configuration du projet

Démarrez chaque projet Expo avec le flux de travail nu (non géré) si vous prévoyez des exigences de module natif. Le flux de travail géré est pratique mais vous limite : le changement ultérieur nécessite une éjection, ce qui est perturbateur.

# Create new Expo project
npx create-expo-app@latest MyApp --template

# Or with TypeScript template
npx create-expo-app@latest MyApp -t expo-template-blank-typescript

# Install core enterprise dependencies
npx expo install \
  expo-router \
  expo-secure-store \
  expo-local-authentication \
  expo-notifications \
  expo-updates \
  expo-constants \
  @react-navigation/native \
  @react-navigation/bottom-tabs \
  @tanstack/react-query

Le système de routage basé sur des fichiers expo-router constitue l'approche moderne : il reflète les modèles de routeur d'applications de Next.js, ce qui facilite grandement le changement de contexte entre le Web et le mobile.


Configuration de l'application.json

Les applications d'entreprise nécessitent une configuration minutieuse de app.json dès le départ :

{
  "expo": {
    "name": "ECOSIRE",
    "slug": "ecosire-mobile",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#0a0a0a"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.ecosire.mobile",
      "buildNumber": "1",
      "infoPlist": {
        "NSFaceIDUsageDescription": "Use Face ID to securely sign in",
        "NSCameraUsageDescription": "Take photos for your profile"
      }
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#0a0a0a"
      },
      "package": "com.ecosire.mobile",
      "versionCode": 1,
      "permissions": [
        "USE_BIOMETRIC",
        "USE_FINGERPRINT",
        "RECEIVE_BOOT_COMPLETED",
        "VIBRATE"
      ]
    },
    "plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#f59e0b",
          "sounds": ["./assets/notification.wav"]
        }
      ],
      "expo-router",
      "expo-secure-store",
      "expo-local-authentication"
    ],
    "updates": {
      "url": "https://u.expo.dev/YOUR-PROJECT-ID"
    },
    "runtimeVersion": {
      "policy": "sdkVersion"
    },
    "extra": {
      "eas": {
        "projectId": "YOUR-PROJECT-ID"
      }
    }
  }
}

Configuration de construction EAS

EAS (Expo Application Services) gère la création, la soumission et la mise à jour de votre application. Configurez-le avec eas.json :

{
  "cli": {
    "version": ">= 10.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": true
      },
      "android": {
        "buildType": "apk"
      },
      "env": {
        "API_URL": "http://localhost:3001",
        "NODE_ENV": "development"
      }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "env": {
        "API_URL": "https://staging-api.ecosire.com",
        "NODE_ENV": "staging"
      }
    },
    "production": {
      "channel": "production",
      "autoIncrement": true,
      "env": {
        "API_URL": "https://api.ecosire.com",
        "NODE_ENV": "production"
      },
      "ios": {
        "credentialsSource": "remote"
      },
      "android": {
        "credentialsSource": "remote"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "[email protected]",
        "ascAppId": "YOUR-APP-STORE-CONNECT-APP-ID",
        "appleTeamId": "YOUR-TEAM-ID"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"
      }
    }
  }
}
# Build for different environments
eas build --profile development --platform ios
eas build --profile preview --platform all
eas build --profile production --platform all

# Submit to stores
eas submit --profile production --platform ios
eas submit --profile production --platform android

Authentification sécurisée

Les applications d'entreprise ne doivent jamais stocker de jetons d'authentification dans AsyncStorage : ils sont non chiffrés et accessibles aux autres applications sur les appareils rootés. Utilisez expo-secure-store qui utilise le trousseau (iOS) et le Keystore (Android) :

// lib/auth/token-storage.ts
import * as SecureStore from 'expo-secure-store';

const ACCESS_TOKEN_KEY = 'ecosire_access_token';
const REFRESH_TOKEN_KEY = 'ecosire_refresh_token';

export const tokenStorage = {
  async saveTokens(accessToken: string, refreshToken: string) {
    await Promise.all([
      SecureStore.setItemAsync(ACCESS_TOKEN_KEY, accessToken, {
        keychainService: 'com.ecosire.mobile.auth',
        keychainAccessible: SecureStore.WHEN_UNLOCKED,
      }),
      SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refreshToken, {
        keychainService: 'com.ecosire.mobile.auth',
        keychainAccessible: SecureStore.WHEN_UNLOCKED,
      }),
    ]);
  },

  async getAccessToken(): Promise<string | null> {
    return SecureStore.getItemAsync(ACCESS_TOKEN_KEY, {
      keychainService: 'com.ecosire.mobile.auth',
    });
  },

  async getRefreshToken(): Promise<string | null> {
    return SecureStore.getItemAsync(REFRESH_TOKEN_KEY, {
      keychainService: 'com.ecosire.mobile.auth',
    });
  },

  async clearTokens() {
    await Promise.all([
      SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY),
      SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY),
    ]);
  },
};

Authentification biométrique

Ajout de Face ID / Touch ID / authentification par empreinte digitale :

// lib/auth/biometric.ts
import * as LocalAuthentication from 'expo-local-authentication';

export async function isBiometricAvailable(): Promise<boolean> {
  const compatible = await LocalAuthentication.hasHardwareAsync();
  if (!compatible) return false;

  const enrolled = await LocalAuthentication.isEnrolledAsync();
  return enrolled;
}

export async function authenticateWithBiometrics(
  reason = 'Confirm your identity'
): Promise<boolean> {
  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: reason,
    fallbackLabel: 'Use passcode',
    disableDeviceFallback: false,
    cancelLabel: 'Cancel',
  });

  return result.success;
}

// Usage in a component
async function handleSensitiveAction() {
  const available = await isBiometricAvailable();

  if (available) {
    const authenticated = await authenticateWithBiometrics(
      'Confirm this transaction'
    );
    if (!authenticated) return;
  }

  // Proceed with the action
  await processSensitiveOperation();
}

Notifications poussées

Les notifications push nécessitent une infrastructure backend et une gestion minutieuse des autorisations :

// lib/notifications/setup.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';

// Configure how notifications appear when app is in foreground
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
});

export async function registerForPushNotifications(): Promise<string | null> {
  // Push notifications don't work on simulators
  if (!Device.isDevice) {
    console.warn('Push notifications require a physical device');
    return null;
  }

  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;

  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') {
    return null; // User denied permissions
  }

  // Android requires a notification channel
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'Default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#f59e0b',
    });
  }

  const projectId = Constants.expoConfig?.extra?.eas?.projectId;
  const token = await Notifications.getExpoPushTokenAsync({ projectId });

  return token.data;
}
// hooks/use-push-notifications.ts
import { useEffect, useRef } from 'react';
import { AppState } from 'react-native';
import * as Notifications from 'expo-notifications';
import { registerForPushNotifications } from '@/lib/notifications/setup';
import { updatePushToken } from '@/lib/api/notifications';

export function usePushNotifications() {
  const notificationListener = useRef<Notifications.Subscription>();
  const responseListener = useRef<Notifications.Subscription>();

  useEffect(() => {
    // Register and save token to backend
    registerForPushNotifications().then((token) => {
      if (token) {
        updatePushToken(token).catch(console.error);
      }
    });

    // Handle notifications received while app is open
    notificationListener.current =
      Notifications.addNotificationReceivedListener((notification) => {
        console.log('Notification received:', notification);
      });

    // Handle notification tap
    responseListener.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        const data = response.notification.request.content.data;
        // Navigate based on notification data
        handleNotificationNavigation(data);
      });

    return () => {
      notificationListener.current?.remove();
      responseListener.current?.remove();
    };
  }, []);
}

Support hors ligne avec WatermelonDB

Les applications d'entreprise doivent fonctionner hors ligne. WatermelonDB est la base de données locale la plus performante pour React Native :

npx expo install @nozbe/watermelondb @nozbe/with-observables
// db/schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb';

export const schema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'contacts',
      columns: [
        { name: 'server_id', type: 'string', isOptional: true },
        { name: 'name', type: 'string' },
        { name: 'email', type: 'string', isOptional: true },
        { name: 'organization_id', type: 'string' },
        { name: 'synced_at', type: 'number', isOptional: true },
        { name: 'is_deleted', type: 'boolean' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' },
      ],
    }),
  ],
});
// lib/sync/contacts-sync.ts
import { synchronize } from '@nozbe/watermelondb/sync';
import { database } from '@/db';

export async function syncContacts(authToken: string) {
  await synchronize({
    database,
    pullChanges: async ({ lastPulledAt }) => {
      const response = await fetch(
        `${API_URL}/sync/contacts?since=${lastPulledAt}`,
        {
          headers: { Authorization: `Bearer ${authToken}` },
        }
      );
      const { changes, timestamp } = await response.json();
      return { changes, timestamp };
    },
    pushChanges: async ({ changes }) => {
      await fetch(`${API_URL}/sync/contacts`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${authToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ changes }),
      });
    },
    migrationsEnabledAtVersion: 1,
  });
}

Requête TanStack pour les données API

TanStack Query fonctionne dans React Native exactement comme dans les applications Web React :

// lib/api/contacts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tokenStorage } from '@/lib/auth/token-storage';

async function apiRequest<T>(path: string, options?: RequestInit): Promise<T> {
  const token = await tokenStorage.getAccessToken();

  const response = await fetch(`${process.env.API_URL}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...options?.headers,
    },
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return response.json();
}

export function useContacts() {
  return useQuery({
    queryKey: ['contacts'],
    queryFn: () => apiRequest<Contact[]>('/contacts'),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useCreateContact() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateContactDto) =>
      apiRequest<Contact>('/contacts', {
        method: 'POST',
        body: JSON.stringify(data),
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['contacts'] });
    },
  });
}

Mises à jour en direct avec mises à jour d'exposition

EAS Update fournit des mises à jour du bundle JavaScript sans examen de l'App Store – essentiel pour les corrections de bugs :

// hooks/use-app-updates.ts
import { useEffect } from 'react';
import * as Updates from 'expo-updates';
import { Alert } from 'react-native';

export function useAppUpdates() {
  useEffect(() => {
    async function checkForUpdates() {
      if (__DEV__) return; // Skip in development

      try {
        const update = await Updates.checkForUpdateAsync();

        if (update.isAvailable) {
          await Updates.fetchUpdateAsync();

          Alert.alert(
            'Update Available',
            'A new version of the app is ready. Restart to apply.',
            [
              { text: 'Later', style: 'cancel' },
              {
                text: 'Restart Now',
                onPress: () => Updates.reloadAsync(),
              },
            ]
          );
        }
      } catch (error) {
        // Update check failed — app continues running current version
        console.warn('Update check failed:', error);
      }
    }

    checkForUpdates();
  }, []);
}

Questions fréquemment posées

Quand dois-je choisir Expo plutôt que React Native ?

Choisissez Expo pour la grande majorité des applications d'entreprise. L'écosystème EAS couvre 95 % des besoins de l'entreprise : authentification biométrique, notifications push, liens profonds, caméra, système de fichiers, stockage sécurisé. Optez pour React Native uniquement si vous avez besoin de modules natifs personnalisés non disponibles dans Expo, ou si vous intégrez du code natif existant. L'expérience de développement avec Expo est nettement meilleure et EAS Build gère la complexité des versions natives.

Comment partager du code entre une application Web Next.js et une application mobile Expo ?

Utilisez un monorepo Turborepo avec des packages partagés pour la logique métier, les types et les clients API. React Native ne prend pas en charge toutes les API de navigateur/Node.js, alors faites attention à ce qui se passe dans les packages partagés. Un modèle qui fonctionne bien : partagez les types, les schémas de validation (Zod) et les fonctions client API, mais gardez les composants de l'interface utilisateur séparés. App Router et Expo Router de Next.js 16 utilisent tous deux un routage basé sur des fichiers avec des modèles mentaux similaires, ce qui facilite la maintenance des implémentations parallèles.

Comment gérer les liens profonds pour les rappels d'authentification ?

Configurez votre schéma d'URI dans app.json sous scheme (par exemple, "scheme": "ecosire"). Enregistrez ecosire://auth/callback comme URI de redirection dans votre fournisseur d'authentification. Utilisez expo-linking pour gérer les URL entrantes dans votre application. Pour les liens universels (iOS) et les liens d'applications (Android) qui fonctionnent comme de vraies URL, vous avez besoin d'une vérification de domaine supplémentaire : les fichiers apple-app-site-association et assetlinks.json servis à partir de votre domaine.

Quelle est la meilleure façon de gérer le contrôle de version des applications dans EAS ?

Définissez "appVersionSource": "remote" dans eas.json et activez "autoIncrement": true pour les versions de production. Cela permet à EAS de gérer automatiquement les numéros de build, évitant ainsi l'erreur courante consistant à oublier d'incrémenter manuellement. Conservez votre version lisible par l'homme (1.2.3) dans app.json et laissez EAS gérer le numéro de build (iOS buildNumber, Android versionCode).

Comment tester les notifications push en cours de développement ?

Utilisez l'outil de test de notification push d'Expo sur https://expo.dev/notifications avec le jeton push Expo de votre appareil. Pour le développement local, expo-notifications fournit une fonction scheduleNotificationAsync qui déclenche des notifications locales sans backend. Pour des tests de bout en bout, créez un client de développement avec eas build --profile development et utilisez un périphérique physique.


Prochaines étapes

Créer une application mobile d'entreprise fiable, sécurisée et maintenable à grande échelle nécessite d'avoir les bonnes bases : la configuration d'EAS Build, le stockage sécurisé des jetons, l'infrastructure de notification push et la synchronisation hors ligne nécessitent tous une configuration minutieuse dès le premier jour.

ECOSIRE a créé des applications mobiles de production à l'aide d'Expo avec 11 écrans, un défilement infini, des notifications push et une configuration de déploiement EAS. Si vous avez besoin d'une expertise en développement mobile, découvrez nos services de développement.

E

Rédigé par

ECOSIRE Research and Development Team

Création de produits numériques de niveau entreprise chez ECOSIRE. Partage d'analyses sur les intégrations Odoo, l'automatisation e-commerce et les solutions d'entreprise propulsées par l'IA.

Discutez sur WhatsApp