docs: add FarmaFinder Mobile design specification
This commit is contained in:
@@ -0,0 +1,420 @@
|
|||||||
|
# FarmaFinder Mobile - Especificación de Diseño
|
||||||
|
|
||||||
|
**Fecha**: 2026-07-06
|
||||||
|
**Estado**: Aprobado
|
||||||
|
**Versión**: 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Resumen
|
||||||
|
|
||||||
|
Crear un segundo frontend en **React Native (Expo)** para FarmaFinder, manteniendo el frontend actual (React + Vite) para Desktop/PWA. El objetivo es lograr una experiencia móvil nativa que permita publicar en App Store y Google Play con mejor rendimiento, UX y mantenibilidad.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Arquitectura
|
||||||
|
|
||||||
|
### 2.1 Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
FarmaFinder/
|
||||||
|
├── frontend/ ← React + Vite (Desktop/PWA, mantiene Capacitor)
|
||||||
|
├── frontend-mobile/ ← Expo + React Native (NUEVO)
|
||||||
|
│ ├── app/ ← Expo Router (file-based routing)
|
||||||
|
│ │ ├── _layout.tsx ← Layout raíz
|
||||||
|
│ │ ├── (tabs)/ ← Bottom tabs
|
||||||
|
│ │ │ ├── _layout.tsx ← Tab navigator
|
||||||
|
│ │ │ ├── index.tsx ← Home (búsqueda)
|
||||||
|
│ │ │ ├── map.tsx ← Mapa de farmacias
|
||||||
|
│ │ │ └── profile.tsx ← Perfil de usuario
|
||||||
|
│ │ ├── medicine/[id].tsx ← Detalle de medicamento
|
||||||
|
│ │ ├── pharmacy/[id].tsx ← Detalle de farmacia
|
||||||
|
│ │ └── auth/ ← Auth flow
|
||||||
|
│ ├── components/ ← Componentes React Native
|
||||||
|
│ ├── services/ ← Lógica de negocio y llamadas API
|
||||||
|
│ ├── hooks/ ← Custom hooks
|
||||||
|
│ ├── constants/ ← Colores, tipos, config
|
||||||
|
│ └── assets/ ← Imágenes, fuentes
|
||||||
|
├── backend/ ← API compartida (sin cambios)
|
||||||
|
└── package.json ← Scripts para ambos frontends
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Stack Tecnológico
|
||||||
|
|
||||||
|
| Componente | Tecnología | Justificación |
|
||||||
|
|------------|------------|---------------|
|
||||||
|
| Framework | Expo SDK 52+ | Setup rápido, OTA updates, fácil publicación |
|
||||||
|
| Routing | Expo Router v4 | File-based routing, similar a Next.js |
|
||||||
|
| Navegación | React Navigation | Bottom tabs + stack navigation |
|
||||||
|
| State | Zustand | Ligero, sin boilerplate, fácil de usar |
|
||||||
|
| HTTP Client | Axios | Interceptors, cancelación de requests |
|
||||||
|
| Cache | TanStack Query | Cache automático, optimistic updates |
|
||||||
|
| Mapas | react-native-maps | Google Maps provider nativo |
|
||||||
|
| Barcode | expo-camera | Escaneo de código de barras nativo |
|
||||||
|
| Storage | AsyncStorage + expo-secure-store | Datos offline + tokens seguros |
|
||||||
|
| Animaciones | React Native Reanimated 3 | Animaciones fluidas en 60fps |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Estructura de Navegación
|
||||||
|
|
||||||
|
### 3.1 Navegación Principal
|
||||||
|
|
||||||
|
```
|
||||||
|
Bottom Tabs
|
||||||
|
├── Home (🔍) → Búsqueda de medicamentos
|
||||||
|
├── Mapa (📍) → Mapa de farmacias
|
||||||
|
└── Perfil (👤) → Perfil de usuario + Admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Navegación Detallada
|
||||||
|
|
||||||
|
```
|
||||||
|
Home Tab (Stack)
|
||||||
|
├── index.tsx ← Lista de resultados de búsqueda
|
||||||
|
└── medicine/[id].tsx ← Detalle de medicamento
|
||||||
|
|
||||||
|
Mapa Tab (Stack)
|
||||||
|
├── map.tsx ← Mapa con marcadores de farmacias
|
||||||
|
└── pharmacy/[id].tsx ← Detalle de farmacia
|
||||||
|
|
||||||
|
Perfil Tab (Stack)
|
||||||
|
├── profile.tsx ← Info de usuario + opciones
|
||||||
|
├── auth/login.tsx ← Login (si no autenticado)
|
||||||
|
├── auth/register.tsx ← Registro
|
||||||
|
└── admin/ ← Panel admin (solo si auth + admin)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Flujos de Navegación
|
||||||
|
|
||||||
|
- **Auth Flow**: Si no hay token → redirigir a login
|
||||||
|
- **Admin Flow**: Si usuario es admin → mostrar acceso a admin
|
||||||
|
- **Deep Linking**: URLs como `farmafinder://medicine/123` abren pantalla específica
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Funcionalidades
|
||||||
|
|
||||||
|
### 4.1 MVP (Paridad con Web)
|
||||||
|
|
||||||
|
| Funcionalidad | Descripción |
|
||||||
|
|---------------|-------------|
|
||||||
|
| **Búsqueda** | Búsqueda en tiempo real de medicamentos vía API CIMA |
|
||||||
|
| **Resultados** | Lista de resultados con nombre, principio activo |
|
||||||
|
| **Detalle medicamento** | Precio, stock, farmacias que lo tienen |
|
||||||
|
| **Mapa farmacias** | Marcadores en mapa con ubicación |
|
||||||
|
| **Detalle farmacia** | Dirección, teléfono, medicamentos disponibles |
|
||||||
|
| **Auth** | Login/registro de usuarios |
|
||||||
|
|
||||||
|
### 4.2 Extras Nativos (NUEVO)
|
||||||
|
|
||||||
|
| Funcionalidad | Descripción |
|
||||||
|
|---------------|-------------|
|
||||||
|
| **Push Notifications** | Alertas de disponibilidad de medicamentos favoritos |
|
||||||
|
| **Escáner código barras** | Escanear medicamentos directamente con cámara |
|
||||||
|
| **Biometría** | Face ID / Touch ID para login rápido |
|
||||||
|
| **Deep Links** | Compartir medicamentos/farmacias via URL |
|
||||||
|
| **Offline Cache** | Medicamentos favoritos accesibles sin conexión |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Integración con la API
|
||||||
|
|
||||||
|
### 5.1 Cliente API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend-mobile/services/api.ts
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE = __DEV__
|
||||||
|
? 'http://localhost:3001/api'
|
||||||
|
: 'https://tu-dominio.com/api';
|
||||||
|
|
||||||
|
export const api = axios.create({
|
||||||
|
baseURL: API_BASE,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor para auth token
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = getToken();
|
||||||
|
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Endpoints
|
||||||
|
|
||||||
|
**Públicos:**
|
||||||
|
- `GET /api/medicines/search?q=<query>` - Búsqueda de medicamentos
|
||||||
|
- `GET /api/medicines/:nregistro` - Detalle de medicamento
|
||||||
|
- `GET /api/medicines/:nregistro/pharmacies` - Farmacias con medicamento
|
||||||
|
- `GET /api/pharmacies` - Todas las farmacias
|
||||||
|
|
||||||
|
**Autenticados:**
|
||||||
|
- `POST /api/auth/login` - Login
|
||||||
|
- `POST /api/auth/logout` - Logout
|
||||||
|
- `GET /api/auth/check` - Verificar auth
|
||||||
|
|
||||||
|
**Admin:**
|
||||||
|
- CRUD de farmacias
|
||||||
|
- Búsqueda de medicamentos para admin
|
||||||
|
- Vincular medicamentos a farmacias
|
||||||
|
|
||||||
|
### 5.3 Estrategia de Cache
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Con TanStack Query
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['medicines', searchQuery],
|
||||||
|
queryFn: () => searchMedicines(searchQuery),
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutos
|
||||||
|
cacheTime: 30 * 60 * 1000, // 30 minutos
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Persistencia offline:**
|
||||||
|
- AsyncStorage para datos no sensibles
|
||||||
|
- expo-secure-store para tokens y credenciales
|
||||||
|
- React Query persistor para cache de queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Diseño Visual
|
||||||
|
|
||||||
|
### 6.1 Design Tokens
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// frontend-mobile/constants/theme.ts
|
||||||
|
export const colors = {
|
||||||
|
// Primary
|
||||||
|
primary: '#007AFF',
|
||||||
|
primaryLight: '#4DA3FF',
|
||||||
|
|
||||||
|
// Semantic
|
||||||
|
success: '#34C759', // Disponible
|
||||||
|
danger: '#FF3B30', // Sin stock
|
||||||
|
warning: '#FF9500', // Stock bajo
|
||||||
|
|
||||||
|
// Neutral
|
||||||
|
background: '#F2F2F7',
|
||||||
|
card: '#FFFFFF',
|
||||||
|
border: '#E5E5EA',
|
||||||
|
|
||||||
|
// Text
|
||||||
|
text: '#1C1C1E',
|
||||||
|
textSecondary: '#8E8E93',
|
||||||
|
textInverse: '#FFFFFF',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const spacing = {
|
||||||
|
xs: 4,
|
||||||
|
sm: 8,
|
||||||
|
md: 16,
|
||||||
|
lg: 24,
|
||||||
|
xl: 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const borderRadius = {
|
||||||
|
sm: 8,
|
||||||
|
md: 12,
|
||||||
|
lg: 16,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Componentes Principales
|
||||||
|
|
||||||
|
| Componente | Descripción |
|
||||||
|
|------------|-------------|
|
||||||
|
| **SearchBar** | Input con debounce (300ms) + dropdown de resultados |
|
||||||
|
| **MedicineCard** | Card con nombre, principio activo, precio, badge de stock |
|
||||||
|
| **PharmacyMarker** | Marcador personalizado en mapa con info window |
|
||||||
|
| **StockBadge** | Badge verde (disponible) / rojo (sin stock) |
|
||||||
|
| **LoadingSpinner** | Spinner nativo durante cargas |
|
||||||
|
| **ErrorToast** | Toast de error con retry |
|
||||||
|
|
||||||
|
### 6.3 Patrones de Diseño
|
||||||
|
|
||||||
|
- **Cards**: Sombra sutil, border-radius 12px
|
||||||
|
- **Tipografía**: Sistema nativo (SF Pro en iOS, Roboto en Android)
|
||||||
|
- **Espaciado**: Escala de 4px base (4, 8, 12, 16, 24, 32)
|
||||||
|
- **Animaciones**: Reanimated 3 para transiciones y gestos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Extras Nativos (Detalles)
|
||||||
|
|
||||||
|
### 7.1 Push Notifications
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Expo Notifications
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
|
||||||
|
// Registrar para notificaciones
|
||||||
|
const registerForPushNotifications = async () => {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
if (status !== 'granted') return;
|
||||||
|
|
||||||
|
const token = await Notifications.getExpoPushToken();
|
||||||
|
return token.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Escuchar notificaciones
|
||||||
|
Notifications.addNotificationReceivedListener((notification) => {
|
||||||
|
// Mostrar alerta de disponibilidad
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Escáner de Código de Barras
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// expo-camera + barcode scanning
|
||||||
|
import { CameraView } from 'expo-camera';
|
||||||
|
|
||||||
|
<CameraView
|
||||||
|
onBarcodeScanned={(data) => {
|
||||||
|
// Buscar medicamento por código de barras
|
||||||
|
searchMedicineByBarcode(data.data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Biometría
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// expo-local-authentication
|
||||||
|
import * as LocalAuthentication from 'expo-local-authentication';
|
||||||
|
|
||||||
|
const authenticateWithBiometrics = async () => {
|
||||||
|
const hasHardware = await LocalAuthentication.hasHardwareAsync();
|
||||||
|
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
|
||||||
|
|
||||||
|
if (hasHardware && isEnrolled) {
|
||||||
|
return await LocalAuthentication.authenticateAsync();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Workflow de Desarrollo
|
||||||
|
|
||||||
|
### 8.1 Scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev:mobile": "cd frontend-mobile && npx expo start",
|
||||||
|
"dev:mobile:android": "cd frontend-mobile && npx expo start --android",
|
||||||
|
"dev:mobile:ios": "cd frontend-mobile && npx expo start --ios",
|
||||||
|
"build:mobile:dev": "cd frontend-mobile && eas build --profile development",
|
||||||
|
"build:mobile:preview": "cd frontend-mobile && eas build --profile preview",
|
||||||
|
"build:mobile:prod": "cd frontend-mobile && eas build --profile production",
|
||||||
|
"submit:android": "cd frontend-mobile && eas submit --platform android",
|
||||||
|
"submit:ios": "cd frontend-mobile && eas submit --platform ios"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 EAS Build Profiles
|
||||||
|
|
||||||
|
```json
|
||||||
|
// frontend-mobile/eas.json
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 CI/CD
|
||||||
|
|
||||||
|
- **Desarrollo**: Expo Go + hot reload
|
||||||
|
- **Testing**: EAS Build (preview) → APK/IPA para testing
|
||||||
|
- **Producción**: EAS Build (production) → Submit a tiendas
|
||||||
|
- **OTA Updates**: Expo Updates para fixes sin pasar por tienda
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Dependencias Principales
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"expo": "~52.0.0",
|
||||||
|
"expo-router": "~4.0.0",
|
||||||
|
"expo-camera": "~16.0.0",
|
||||||
|
"expo-notifications": "~0.29.0",
|
||||||
|
"expo-local-authentication": "~15.0.0",
|
||||||
|
"expo-secure-store": "~14.0.0",
|
||||||
|
"react-native": "0.76.0",
|
||||||
|
"react-native-maps": "1.18.0",
|
||||||
|
"react-native-reanimated": "~3.16.0",
|
||||||
|
"react-native-gesture-handler": "~2.20.0",
|
||||||
|
"@react-navigation/native": "^7.0.0",
|
||||||
|
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||||
|
"zustand": "^5.0.0",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"@tanstack/react-query": "^5.0.0",
|
||||||
|
"@react-native-async-storage/async-storage": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Criterios de Aceptación
|
||||||
|
|
||||||
|
### MVP
|
||||||
|
- [ ] Búsqueda de medicamentos funcional
|
||||||
|
- [ ] Mapa con farmacias mostrando marcadores
|
||||||
|
- [ ] Detalle de medicamento con precio y stock
|
||||||
|
- [ ] Detalle de farmacia con ubicación
|
||||||
|
- [ ] Login/registro funcional
|
||||||
|
- [ ] Navegación entre pantallas fluida
|
||||||
|
- [ ] Build de desarrollo funcionando en Expo Go
|
||||||
|
|
||||||
|
### Extras Nativos
|
||||||
|
- [ ] Push notifications configuradas
|
||||||
|
- [ ] Escáner de código de barras funcional
|
||||||
|
- [ ] Biometría para login rápido
|
||||||
|
- [ ] Deep links configurados
|
||||||
|
- [ ] Offline cache para favoritos
|
||||||
|
|
||||||
|
### Publicación
|
||||||
|
- [ ] Build de producción optimizado
|
||||||
|
- [ ] App Store listing configurado
|
||||||
|
- [ ] Google Play listing configurado
|
||||||
|
- [ ] Screenshots y descripción preparados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Riesgos y Mitigaciones
|
||||||
|
|
||||||
|
| Riesgo | Mitigación |
|
||||||
|
|--------|------------|
|
||||||
|
| API del backend no preparada para móvil | Revisar endpoints, agregar si es necesario |
|
||||||
|
| Performance en dispositivos antiguos | Testing en dispositivos低端, optimizar renders |
|
||||||
|
| Diferencias iOS/Android | Usar componentes nativos, testing en ambas plataformas |
|
||||||
|
| Mantenimiento de dos frontends | Documentar bien, compartir types cuando sea posible |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Próximos Pasos
|
||||||
|
|
||||||
|
1. Crear estructura del proyecto Expo
|
||||||
|
2. Configurar routing con Expo Router
|
||||||
|
3. Implementar cliente API
|
||||||
|
4. Crear pantallas MVP
|
||||||
|
5. Implementar extras nativos
|
||||||
|
6. Testing y optimización
|
||||||
|
7. Build y publicación en tiendas
|
||||||
Reference in New Issue
Block a user