Skip to main content

Visión general

oxitec-mobile-app es la app del transportista: recibe la hoja de ruta, entrega y retira equipos en el domicilio del paciente, escanea seriales, gestiona el stock del vehículo y hace firmar el remito. Nombre en las tiendas: BullStock.

Stack

RuntimeReact 19 · React Native 0.81.5 · Expo SDK 54 (Hermes, new architecture)
Ruteoexpo-router 6 (file-based)
Estado servidor@tanstack/react-query 5
Base localexpo-sqlite (bullstock.db)
Auth@clerk/clerk-expo + expo-secure-store · Firebase como fallback conmutable
EstilosUniwind (Tailwind v4) — ver UI y estilos
UIHeroUI Native (nuevo) + Gluestack (legacy)
Localees-AR
  • iOS bundle: com.nwyler-4quinas.bullstock
  • Android package: com.nwyler_4quinas.bullstock

Estructura

app/ # rutas (expo-router)
├── index.tsx # boot: redirige según auth + mobile-config
├── home.tsx profile.tsx
├── auth/ login, forgot-password
├── (roadmap)/ roadmap, roadmapTask, roadmapVerifyDelivery, scanInventariable,
│ scanVerifyDelivery, scanVerifyRetirement, failedTask, successTask,
│ patientEventComment
├── (inventory)/ inventoryMenu, stockByElement, stockByProduct, stockSerials,
│ serialSearch, addStock, selectProducts, warehouseSelection
├── (vehicle)/ vehicle, vehicleQrScan, vehicleStock, vehicleStockMovement
├── (patient)/ patientInfo
└── (signature)/ digitalSignature, digitalSignatureForm

components/ ui-native/ (HeroUI) · ui/ (Gluestack) · shared/<categoría>/ · <feature>/
services/ api, auth, database/, offlineQueue, syncService, networkMonitor, mobileConfig
hooks/ shared/ · <dominio>/<pantalla>/
lib/ repositorios de dominio y utilidades con estado
helpers/ funciones puras
contexts/ Theme, MobileConfig, AppVersion, Drawer, NavigationLoading, ProductStatusOptions

Árbol de providers

app/_layout.tsx, de afuera hacia adentro. El orden importa — cada uno depende del contexto del anterior:

SafeAreaProvider → ThemeProvider → ClerkProvider → AuthBridgeProvider
→ MobileConfigProvider → AppVersionProvider → ProductStatusOptionsProvider
→ QueryClientProvider → ToastProvider → HeroUINativeProvider
→ GluestackUIProvider → NavigationLoadingProvider → DrawerProvider

Al montar corre initDatabase(), arranca startSyncService(), registra push de Expo y aplica un Text.defaultProps = { textBreakStrategy: 'simple' } para Android.

No saques el textBreakStrategy

Arregla el clipping de texto en varios modelos de Samsung, Pixel y OnePlus. Parece un workaround raro y lo es, pero sin él hay usuarios que no leen la mitad de las pantallas.

QueryClient: staleTime 5 min, retry: 2, refetchOnWindowFocus: false.

Capa HTTP

services/api.ts es un wrapper propio sobre fetch, no Axios. Todo sale por apiRequest():

  • Timeout 60 s por default.
  • Reintentos con backoff exponencial; no reintenta 4xx salvo 408.
  • Inyecta el token de Clerk cuando includeAuth, y reintenta un 401 con token fresco si retryOnAuthError.
  • Chequea conectividad antes de disparar.
  • Lee EXPO_PUBLIC_API_URL de process.env, con fallback a expoConfig.extra.

Encima está hooks/shared/useApi.ts (authenticatedRequest / queryFn, estabilizados con useCallback) y los hooks de React Query.

El default de retries es 0, no 3

El archivo calcula un defaultRetries (3, o 0 si EXPO_PUBLIC_DISABLE_API_RETRIES=true) pero nunca lo usa: la desestructuración es retries = 0. Si querés reintentos, pasalos explícitamente en el config.

Auth

Clerk es el proveedor primario; services/authProviderConfig.ts + contexts/AuthBridgeContext.tsx permiten que el backend conmute el cliente a Firebase en runtime.

Siempre pasá por services/auth.ts (getValidToken() / handleAuthError()); no llames hooks de Clerk desde código de API. Ver Autenticación.

Mobile config y versión

services/mobileConfig.ts trae la config del dispositivo desde el módulo mobileConfig del backend y decide qué features están habilitadas. Gateá features contra esa config, no con flags hardcodeados.

En dev lee de .env (EXPO_PUBLIC_MOBILE_CONFIG_*), así que cambiar el comportamiento dev↔prod es cambiar de archivo .env, no de código.

AppVersionContext + helpers/compareSemver.ts deciden si mostrar el modal de actualización obligatoria.

Reglas de React Native que evitan crashes

No son estilo: son crashes reales en producción.

  • Nunca {value && <Component />} cuando value pueda ser 0 o "". Usá ternario o !!value. Un 0 suelto se renderiza como texto fuera de <Text> y crashea.
  • Todo string va dentro de <Text>. Nunca suelto bajo un <View>.
  • Pressable en vez de TouchableOpacity.
  • Memoizá los ítems de lista con React.memo; nada de objetos o funciones inline en renderItem.
  • Animá solo transform y opacity.
  • removeClippedSubviews={false} en las FlatList — estaba causando un NPE nativo (dispatchGetDisplayList con hijo null). Está en false a propósito: no lo "optimices".
  • Navegación desde handlers asyncuseSafeNavigation. Y evitá router.replace hacia la ruta en la que ya estás.

Hooks: modulares, no monolito

Nada nuevo va a hooks/useApiQueries.ts. Ese archivo ya es enorme y solo se toca para lo que depende de su infraestructura (la mutation offline y el dedup de taps).

Lo nuevo va a hooks/<dominio>/<pantalla>/useX.ts, con las constantes en constants/<dominio>/ y las query keys centralizadas por dominio (hooks/inventory/queryKeys.ts).

Sin tests, sin lint

package.json no tiene test runner, linter ni script de formato. La validación es npx tsc --noEmit

  • build + prueba en dispositivo.

Helpers que ya existen

Varios nacieron de un bug real. Antes de escribir uno nuevo, mirá helpers/:

HelperQué resuelve
productLabel.tsEtiqueta legible de un producto (descripción → nombre → SKU → Producto <id>), null-safe. Existe porque name + (attr ? ' - ' + attr : '') || fallback con description NULL daba el string "null", que es truthy y se comía el fallback: el operario veía filas que decían null
sessionError.tsError tipado de sesión expirada. Antes ese caso devolvía null y las mutations crasheaban leyendo .savedOffline sobre null
errorMessages.tsElige el título del alert. El cuerpo siempre viene del backend; este helper nunca reescribe ni tapa un error
safeJsonParse.tsJSON.parse defensivo (descarta "", "undefined", "null")
compareSemver.tsCompara N.N.N; devuelve 0 si no parsea, para no bloquear al usuario por un valor inválido del backend
recalcVehicleStockAlert.tsEl texto de "cantidad insuficiente" que pidió el cliente + el alert con opción de recalcular
lib/utils.tscn(...)twMerge(clsx(...))
lib/states.tsDispatchStates: los ids numéricos de estado de la parada. Espejan EntityStatus del backend: si alguien reparametriza estados, hay que tocar acá