Creación de aplicaciones móviles empresariales con Expo y React Native
Expo ha pasado de ser una herramienta de creación rápida de prototipos a una plataforma empresarial legítima. Con Expo SDK 52 y la nueva arquitectura (representador Fabric + JSI), puede crear aplicaciones móviles multiplataforma que igualen el rendimiento y la sensación de las aplicaciones nativas, mientras comparte código con su aplicación web React y lo envía a iOS y Android desde una única base de código TypeScript.
Esta guía cubre los patrones que separan un proyecto de hobby de una aplicación empresarial de producción: configuración de compilación EAS, infraestructura de notificaciones push, sincronización de datos sin conexión, autenticación biométrica y flujos de trabajo de envío de App Store.
Conclusiones clave
- Expo Go es para demostraciones; EAS Build es para producción: comience con EAS desde el primer día
- La nueva arquitectura (Fabric + JSI) es opcional pero ofrece mejoras de rendimiento mensurables
expo-notificationsrequiere una configuración cuidadosa para el manejo tanto en primer plano como en segundo plano- El soporte sin conexión necesita una base de datos local (WatermelonDB o SQLite) con lógica de sincronización
- La autenticación biométrica con
expo-local-authenticationson unas pocas líneas de código- Los enlaces profundos requieren configuración tanto en app.json como en las URL de devolución de llamada de su proveedor de autenticación
- Los alias de ruta de TypeScript funcionan en Expo a través de
babel-plugin-module-resolver- Nunca almacene datos confidenciales en
AsyncStorage; useexpo-secure-storepara tokens
Configuración del proyecto
Inicie cada proyecto de Expo con el flujo de trabajo básico (no administrado) si anticipa algún requisito del módulo nativo. El flujo de trabajo administrado es conveniente pero lo limita: cambiar más tarde requiere expulsión, lo cual es perjudicial.
# 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
El sistema de enrutamiento basado en archivos expo-router es el enfoque moderno: refleja los patrones de App Router de Next.js, lo que facilita mucho el cambio de contexto entre la web y el móvil.
Configuración de app.json
Las aplicaciones empresariales necesitan una configuración app.json cuidadosa desde el principio:
{
"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"
}
}
}
}
Configuración de compilación EAS
EAS (Expo Application Services) se encarga de crear, enviar y actualizar su aplicación. Configúrelo con 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
Autenticación segura
Las aplicaciones empresariales nunca deben almacenar tokens de autenticación en AsyncStorage: no están cifrados y son accesibles para otras aplicaciones en dispositivos rooteados. Utilice expo-secure-store que utiliza Keychain (iOS) y 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),
]);
},
};
Autenticación biométrica
Agregar Face ID/Touch ID/autenticación de huellas dactilares:
// 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();
}
Notificaciones automáticas
Las notificaciones automáticas requieren una infraestructura de backend y un manejo cuidadoso de los permisos:
// 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();
};
}, []);
}
Soporte sin conexión con WatermelonDB
Las aplicaciones empresariales deben funcionar sin conexión. WatermelonDB es la base de datos local de mayor rendimiento 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 datos API
TanStack Query funciona en React Native exactamente como en las aplicaciones web de 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'] });
},
});
}
Actualizaciones inalámbricas con actualizaciones de exposición
EAS Update ofrece actualizaciones del paquete JavaScript sin revisión de la App Store, algo fundamental para corregir errores:
// 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();
}, []);
}
Preguntas frecuentes
¿Cuándo debo elegir Expo en lugar de React Native?
Elija Expo para la gran mayoría de aplicaciones empresariales. El ecosistema EAS cubre el 95 % de los requisitos empresariales: autenticación biométrica, notificaciones automáticas, enlaces profundos, cámara, sistema de archivos y almacenamiento seguro. Utilice React Native solo si necesita módulos nativos personalizados que no están disponibles en Expo o si se está integrando con código nativo heredado. La experiencia de desarrollo con Expo es significativamente mejor y EAS Build maneja la complejidad de las compilaciones nativas.
¿Cómo comparto código entre una aplicación web Next.js y una aplicación móvil Expo?
Utilice un monorepo Turborepo con paquetes compartidos para lógica empresarial, tipos y clientes API. React Native no es compatible con todas las API de navegador/Node.js, así que tenga cuidado con lo que incluye los paquetes compartidos. Un patrón que funciona bien: comparte tipos, esquemas de validación (Zod) y funciones de cliente API, pero mantiene los componentes de la interfaz de usuario separados. App Router y Expo Router de Next.js 16 utilizan enrutamiento basado en archivos con modelos mentales similares, lo que facilita el mantenimiento de implementaciones paralelas.
¿Cómo manejo los enlaces profundos para las devoluciones de llamadas de autenticación?
Configure su esquema de URI en app.json bajo scheme (por ejemplo, "scheme": "ecosire"). Registre ecosire://auth/callback como URI de redireccionamiento en su proveedor de autenticación. Utilice expo-linking para manejar las URL entrantes en su aplicación. Para enlaces universales (iOS) y enlaces de aplicaciones (Android) que funcionan como URL reales, necesita verificación de dominio adicional: archivos apple-app-site-association y assetlinks.json servidos desde su dominio.
¿Cuál es la mejor manera de manejar el control de versiones de aplicaciones en EAS?
Establezca "appVersionSource": "remote" en eas.json y habilite "autoIncrement": true para compilaciones de producción. Esto permite a EAS administrar los números de compilación automáticamente, evitando el error común de olvidarse de incrementarlos manualmente. Mantenga su versión legible por humanos (1.2.3) en app.json y deje que EAS maneje el número de compilación (iOS buildNumber, Android versionCode).
¿Cómo pruebo las notificaciones push en desarrollo?
Utilice la herramienta de prueba de notificaciones push de Expo en https://expo.dev/notifications con el token push de Expo de su dispositivo. Para el desarrollo local, expo-notifications proporciona una función scheduleNotificationAsync que activa notificaciones locales sin un backend. Para pruebas de un extremo a otro, cree un cliente de desarrollo con eas build --profile development y utilice un dispositivo físico.
Próximos pasos
Crear una aplicación móvil empresarial que sea confiable, segura y mantenible a escala requiere tener las bases adecuadas: la configuración de EAS Build, el almacenamiento seguro de tokens, la infraestructura de notificaciones push y la sincronización fuera de línea necesitan una configuración cuidadosa desde el primer día.
ECOSIRE ha creado aplicaciones móviles de producción utilizando Expo con 11 pantallas, desplazamiento infinito, notificaciones automáticas y configuración de implementación de EAS. Si necesita experiencia en desarrollo móvil, explore nuestros servicios de desarrollo.
Escrito por
ECOSIRE Research and Development Team
Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.
Artículos relacionados
Data Mesh Architecture: Decentralized Data for Enterprise
A comprehensive guide to data mesh architecture—principles, implementation patterns, organizational requirements, and how it enables scalable, domain-driven data ownership.
ECOSIRE vs Big 4 Consultancies: Enterprise Quality, Startup Speed
How ECOSIRE delivers enterprise-grade ERP and digital transformation outcomes without Big 4 pricing, overhead, or timeline bloat. A direct comparison.
Generative AI in Enterprise Applications: Beyond Chatbots
Discover how generative AI is transforming enterprise applications beyond chatbots—from code generation to synthetic data, document intelligence, and process automation.