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 de março de 202610 min de leitura2.2k Palavras|

Construindo aplicativos móveis empresariais com Expo e React Native

A Expo amadureceu de uma ferramenta de prototipagem rápida para uma plataforma empresarial legítima. Com Expo SDK 52 e a nova arquitetura (renderizador Fabric + JSI), você pode criar aplicativos móveis multiplataforma que correspondem ao desempenho e à sensação de aplicativos nativos - enquanto compartilha código com seu aplicativo Web React e envia para iOS e Android a partir de uma única base de código TypeScript.

Este guia aborda os padrões que separam um projeto de hobby de um aplicativo empresarial de produção: configuração do EAS Build, infraestrutura de notificação push, sincronização de dados offline, autenticação biométrica e fluxos de trabalho de envio da App Store.

Principais conclusões

  • Expo Go é para demonstrações; O EAS Build é para produção – comece com o EAS desde o primeiro dia
  • A nova arquitetura (Fabric + JSI) é opcional, mas oferece ganhos de desempenho mensuráveis
  • expo-notifications requer configuração cuidadosa para manipulação em primeiro e segundo plano
  • O suporte offline precisa de um banco de dados local (WatermelonDB ou SQLite) com lógica de sincronização
  • A autenticação biométrica com expo-local-authentication consiste em algumas linhas de código
  • O link direto requer configuração em app.json e nos URLs de retorno de chamada do seu provedor de autenticação
  • Aliases de caminho TypeScript funcionam na Expo via babel-plugin-module-resolver
  • Nunca armazene dados confidenciais em AsyncStorage — use expo-secure-store para tokens

Configuração do projeto

Inicie cada projeto Expo com o fluxo de trabalho Bare (não gerenciado) se você antecipar algum requisito de módulo nativo. O fluxo de trabalho gerenciado é conveniente, mas limita você: a troca posterior requer a ejeção, o que é perturbador.

# 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

O sistema de roteamento baseado em arquivo expo-router é a abordagem moderna - ele reflete os padrões do App Router do Next.js, tornando a alternância de contexto entre web e dispositivos móveis muito mais fácil.


configuração do app.json

Os aplicativos corporativos precisam de uma configuração app.json cuidadosa desde o início:

{
  "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"
      }
    }
  }
}

Configuração de compilação EAS

EAS (Expo Application Services) cuida da construção, envio e atualização do seu aplicativo. Configure-o com 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

Autenticação segura

Os aplicativos empresariais nunca devem armazenar tokens de autenticação em AsyncStorage — eles não são criptografados e podem ser acessados ​​por outros aplicativos em dispositivos com acesso root. Use expo-secure-store que usa Keychain (iOS) e 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),
    ]);
  },
};

Autenticação Biométrica

Adicionando autenticação de Face ID/Touch ID/impressão digital:

// 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();
}

Notificações push

Notificações push requerem infraestrutura de back-end e tratamento cuidadoso de permissões:

// 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();
    };
  }, []);
}

Suporte offline com WatermelonDB

Os aplicativos empresariais precisam funcionar offline. WatermelonDB é o banco de dados local de melhor desempenho para 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,
  });
}

Consulta TanStack para dados de API

O TanStack Query funciona no React Native exatamente como nos aplicativos da 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'] });
    },
  });
}

Atualizações Over-the-Air com expo-updates

A Atualização EAS oferece atualizações de pacotes JavaScript sem revisão da App Store – fundamental para correções 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();
  }, []);
}

Perguntas frequentes

Quando devo escolher Expo em vez de React Native?

Escolha Expo para a grande maioria dos aplicativos empresariais. O ecossistema EAS cobre 95% dos requisitos empresariais — autenticação biométrica, notificações push, links diretos, câmera, sistema de arquivos, armazenamento seguro. Use o React Native apenas se precisar de módulos nativos personalizados não disponíveis no Expo ou se estiver integrando com código nativo legado. A experiência de desenvolvimento com Expo é significativamente melhor e o EAS Build lida com a complexidade das compilações nativas.

Como compartilho código entre um aplicativo da web Next.js e um aplicativo móvel Expo?

Use um monorepo Turborepo com pacotes compartilhados para lógica de negócios, tipos e clientes API. O React Native não oferece suporte a todas as APIs do navegador/Node.js, portanto, tome cuidado com o que acontece nos pacotes compartilhados. Um padrão que funciona bem: compartilhe tipos, esquemas de validação (Zod) e funções de cliente de API, mas mantenha os componentes da UI separados. O App Router e o Expo Router do Next.js 16 usam roteamento baseado em arquivo com modelos mentais semelhantes, tornando mais fácil manter implementações paralelas.

Como lidar com links diretos para retornos de chamada de autenticação?

Configure seu esquema de URI em app.json em scheme (por exemplo, "scheme": "ecosire"). Registre ecosire://auth/callback como um URI de redirecionamento em seu provedor de autenticação. Use expo-linking para lidar com URLs recebidos em seu aplicativo. Para links universais (iOS) e links de aplicativos (Android) que funcionam como URLs reais, você precisa de verificação de domínio adicional – arquivos apple-app-site-association e assetlinks.json veiculados em seu domínio.

Qual é a melhor maneira de lidar com o versionamento de aplicativos no EAS?

Defina "appVersionSource": "remote" em eas.json e habilite "autoIncrement": true para compilações de produção. Isso permite que o EAS gerencie os números de compilação automaticamente, evitando o erro comum de esquecer de incrementar manualmente. Mantenha sua versão legível (1.2.3) em app.json e deixe o EAS lidar com o número da compilação (iOS buildNumber, Android versionCode).

Como faço para testar notificações push em desenvolvimento?

Use a ferramenta de teste de notificação push da Expo em https://expo.dev/notifications com o token push Expo do seu dispositivo. Para desenvolvimento local, expo-notifications fornece uma função scheduleNotificationAsync que aciona notificações locais sem back-end. Para testes ponta a ponta, crie um cliente de desenvolvimento com eas build --profile development e use um dispositivo físico.


Próximas etapas

Construir um aplicativo móvel corporativo que seja confiável, seguro e de fácil manutenção em escala requer a base correta: a configuração do EAS Build, o armazenamento seguro de tokens, a infraestrutura de notificação por push e a sincronização offline precisam de uma configuração cuidadosa desde o primeiro dia.

A ECOSIRE construiu aplicativos móveis de produção usando Expo com 11 telas, rolagem infinita, notificações push e configuração de implantação EAS. Se você precisar de experiência em desenvolvimento móvel, explore nossos serviços de desenvolvimento.

E

Escrito por

ECOSIRE Research and Development Team

Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.

Converse no WhatsApp