Guía de Desarrollo
BioSello en React Native (JavaScript)

Aprende a construir la app móvil de trazabilidad cárnica desde cero — desde los fundamentos de React Native hasta el despliegue del APK final.


📗 5 módulos básicos 📒 4 módulos intermedios 📕 6 módulos avanzados
⚛️
01
Intro React Native
Qué es, por qué usarlo, instalación del entorno.
📁
02
Estructura del Proyecto
Organización de carpetas y archivos del proyecto BioSello.
🧩
03
Componentes y Hooks
Props, state, useState, useEffect y hooks personalizados.
🗺️
04
Navegación
React Navigation: Stack, Tab y Drawer para BioSello.
🎨
05
Estilos
StyleSheet, Flexbox, temas y diseño del dashboard.
📝
06
Formularios
Registro de animales, guías de tránsito y validaciones.
🌐
07
Consumo de APIs
fetch, Axios, manejo de errores y estados de carga.
💾
08
Almacenamiento
AsyncStorage, Zustand y caché offline para lotes.
🏛️
09
Arquitectura
Patrones, separación de responsabilidades y clean code.
🗄️
10
Backend y MariaDB
Conexión a la BD biosello y diseño del schema.
🔌
11
API REST
Node.js + Express: endpoints para animales, lotes y guías.
🔐
12
Autenticación JWT
Login, roles admin/empleado/cliente y sesiones seguras.
📷
13
Códigos QR
Generación, escaneo y trazabilidad por lote.
🥽
14
Realidad Aumentada
ViroReact / AR para visualizar info del lote en cámara.
📦
15
Despliegue y APK
Build, firma y publicación de la app BioSello.
01
Introducción a React Native
Fundamentos, ventajas y configuración del entorno de desarrollo
⬤ Básico

¿Qué es React Native?

React Native es un framework de Meta que permite construir aplicaciones móviles nativas para Android e iOS usando JavaScript y la misma lógica de React. En lugar de generar una WebView, compila componentes que mapean directamente a widgets nativos del SO — por eso las apps se sienten fluidas.

🐄
BioSello: Usaremos React Native para que inspectores, empleados de rastro y clientes puedan acceder a la trazabilidad cárnica desde su celular, escaneando el código QR del lote y viendo todos los datos del animal.

React Native vs alternativas

CriterioReact NativeFlutterNativo Android/iOS
LenguajeJavaScript / TypeScriptDartKotlin / Swift
RendimientoMuy bueno (bridge)ExcelenteExcelente
Curva aprendizajeBaja (si conoces React)MediaAlta
ComunidadEnormeGrandeGrande
Código compartido~90 %~95 %0 %

Instalación del entorno

Node.js LTS — Descarga desde nodejs.org. Verifica con node -v y npm -v.
React Native CLInpm install -g react-native-cli
Android Studio — Instala el SDK de Android, configura ANDROID_HOME en tu PATH.
JDK 17 — Necesario para compilar el proyecto Android.
Crear el proyecto — Ejecuta el comando de abajo.
bash — Terminal
# Crear el proyecto BioSello
npx react-native@latest init BioSello

# Entrar al proyecto
cd BioSello

# Correr en Android (con emulador o dispositivo conectado)
npx react-native run-android
⚠️
Nota: Si usas Windows, asegúrate de que las variables ANDROID_HOME y JAVA_HOME estén configuradas. Si usas Mac, puedes ejecutar npx react-native run-ios también.

Hola BioSello — primer componente

App.jsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>🐄 BioSello</Text>
      <Text style={styles.sub}>Trazabilidad cárnica en tu mano</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#0d1117' },
  title:     { fontSize: 32, fontWeight: 'bold', color: '#3fb950' },
  sub:       { fontSize: 16, color: '#8b949e', marginTop: 8 },
});
✏️ Ejercicio 01

Tu primer componente BioSello

  • Crea la pantalla de inicio mostrando el número de arete 1301226566 y la especie BOVINO.
  • Agrega un botón que muestre en consola "Animal registrado".
  • Cambia el color de fondo según el sexo: verde para HEMBRA, azul para MACHO.
02
Estructura del Proyecto
Organización modular pensada para BioSello
⬤ Básico

Árbol de archivos recomendado

Estructura de carpetas — BioSello/
BioSello/
├── android/               # Código nativo Android
├── ios/                   # Código nativo iOS
├── src/
│   ├── api/               # Llamadas HTTP al backend
│   │   ├── animales.js
│   │   ├── lotes.js
│   │   ├── auth.js
│   │   └── client.ts      # Instancia Axios configurada
│   ├── components/        # Componentes reutilizables
│   │   ├── AnimalCard.jsx
│   │   ├── LoteCard.jsx
│   │   ├── QRScanner.jsx
│   │   └── AlertaBadge.jsx
│   ├── screens/           # Una pantalla = un archivo
│   │   ├── LoginScreen.jsx
│   │   ├── DashboardScreen.jsx
│   │   ├── AnimalDetailScreen.jsx
│   │   ├── LoteFormScreen.jsx
│   │   ├── GuiaTransitoScreen.jsx
│   │   └── QRResultScreen.jsx
│   ├── navigation/        # Configuración de rutas
│   │   ├── AppNavigator.jsx
│   │   ├── AuthNavigator.jsx
│   │   └── TabNavigator.jsx
│   ├── store/             # Estado global (Zustand)
│   │   ├── authStore.js
│   │   └── loteStore.js
│   ├── hooks/             # Hooks personalizados
│   │   ├── useAnimales.js
│   │   └── useAlerts.js
│   ├── utils/             # Funciones utilitarias
│   │   ├── validators.js
│   │   └── formatters.js
│   ├── constants/
│   │   └── theme.ts       # Colores y tipografía
│   └── types/
│       └── index.ts       # Tipos TypeScript del dominio
├── App.jsx
├── package.json
└── babel.config.js

Tipos del dominio BioSello

src/constants/shapes.js
// En JavaScript puro no hay interfaces.
// Documentamos la forma de los objetos con JSDoc:

/**
 * @typedef {Object} Animal
 * @property {number} id_animal
 * @property {string} num_arete
 * @property {'BOVINO'|'PORCINO'|'OVINO'|'CAPRINO'|'EQUINO'} especie
 * @property {'MACHO'|'HEMBRA'} sexo
 * @property {'VAQUILLA'|'VACA'|'TORETE'|'TORO'|'BECERRO'|'BECERRA'|'BUEY'} clasificacion
 * @property {number} meses_edad
 * @property {boolean} arete_faltante
 * @property {string} [foto_url]
 * @property {number} id_origen
 * @property {number} id_propietario
 * @property {string} created_at
 */

/**
 * @typedef {Object} Lote
 * @property {number} id_lote
 * @property {string} codigo_lote
 * @property {string} tipo_corte
 * @property {number} peso_kg
 * @property {string} fecha_ingreso
 * @property {string} fecha_vencimiento
 * @property {'activo'|'procesado'|'vendido'|'caducado'} estado
 * @property {number} [id_animal]
 * @property {string} [foto_url]
 */

/**
 * @typedef {Object} Usuario
 * @property {number} id_usuario
 * @property {string} nombre
 * @property {string} email
 * @property {'admin'|'empleado'|'cliente'} perfil
 * @property {boolean} activo
 */
✏️ Ejercicio 02

Crea la estructura en tu proyecto

  • Ejecuta mkdir -p src/{api,components,screens,navigation,store,hooks,utils,constants,types}
  • Crea el archivo src/constants/shapes.js con los comentarios JSDoc mostrados.
  • Agrega los typedef JSDoc de Propietario y Origen basándote en el schema de la BD.
03
Componentes, Props, State y Hooks
Los bloques fundamentales de cualquier pantalla de BioSello
⬤ Básico

Props — AnimalCard

Las props son los parámetros que un componente padre le pasa a un hijo. Son de solo lectura.

src/components/AnimalCard.jsx
import React from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
import { Animal } from '../constants/shapes';

// Las props se pasan sin tipado estático en JS
export default function AnimalCard({ animal, onPress }) {
  const esHembra = animal.sexo === 'HEMBRA';

  return (
    <TouchableOpacity style={styles.card} onPress={onPress}>
      <View style={[styles.badge, esHembra ? styles.hembra : styles.macho]}>
        <Text style={styles.badgeText}>{animal.sexo}</Text>
      </View>
      <Text style={styles.arete}>🏷 {animal.num_arete}</Text>
      <Text style={styles.info}>{animal.especie} · {animal.clasificacion}</Text>
      <Text style={styles.edad}>{animal.meses_edad} meses</Text>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  card:      { backgroundColor: '#161b22', borderRadius: 10, padding: 16, marginBottom: 12, borderWidth: 1, borderColor: '#30363d' },
  badge:     { alignSelf: 'flex-start', paddingHorizontal: 10, paddingVertical: 3, borderRadius: 20, marginBottom: 8 },
  hembra:    { backgroundColor: 'rgba(63,185,80,.2)' },
  macho:     { backgroundColor: 'rgba(88,166,255,.2)' },
  badgeText: { fontSize: 11, fontWeight: '700', color: '#e6edf3' },
  arete:     { fontSize: 18, fontWeight: 'bold', color: '#e6edf3' },
  info:      { fontSize: 14, color: '#8b949e', marginTop: 4 },
  edad:      { fontSize: 13, color: '#3fb950', marginTop: 6 },
});

useState — Formulario de arete

Uso de useState
import React, { useState } from 'react';
import { View, TextInput, Text, Button, StyleSheet } from 'react-native';

export default function BuscarAnimal() {
  // [valor actual, función para actualizar]
  const [arete, setArete] = useState('');
  const [error, setError] = useState(null);

  const validar = () => {
    if (arete.length < 10) {
      setError('El número de arete debe tener al menos 10 caracteres');
    } else {
      setError(null);
      // llamar API de búsqueda...
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        style={[styles.input, error && styles.inputError]}
        placeholder="Ej: 1301226566"
        placeholderTextColor="#8b949e"
        value={arete}
        onChangeText={setArete}
        keyboardType="numeric"
      />
      {error && <Text style={styles.error}>{error}</Text>}
      <Button title="Buscar animal" onPress={validar} color="#3fb950" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  input:     { borderWidth: 1, borderColor: '#30363d', borderRadius: 8, padding: 12, color: '#e6edf3', backgroundColor: '#161b22', marginBottom: 8 },
  inputError:{ borderColor: '#f78166' },
  error:     { color: '#f78166', fontSize: 12, marginBottom: 8 },
});

useEffect — Cargar lotes al montar

useEffect para carga de datos
import React, { useState, useEffect } from 'react';
import { View, FlatList, ActivityIndicator } from 'react-native';
import { Lote } from '../constants/shapes';
import { getLotes } from '../api/lotes';
import LoteCard from '../components/LoteCard';

export default function DashboardScreen() {
  const [lotes, setLotes]   = useState([]);
  const [cargando, setCargando] = useState(true);

  // Se ejecuta al montar el componente
  useEffect(() => {
    const cargarDatos = async () => {
      try {
        const data = await getLotes();
        setLotes(data);
      } catch(e) {
        console.error('Error cargando lotes', e);
      } finally {
        setCargando(false);
      }
    };
    cargarDatos();
  }, []); // [] = solo se ejecuta una vez

  if (cargando) return <ActivityIndicator color="#3fb950" />;

  return (
    <FlatList
      data={lotes}
      keyExtractor={item => String(item.id_lote)}
      renderItem={({ item }) => <LoteCard lote={item} />}
    />
  );
}

Hook personalizado — useAnimales

src/hooks/useAnimales.ts
import { useState, useEffect, useCallback } from 'react';
import { Animal } from '../constants/shapes';
import { getAnimales } from '../api/animales';

export function useAnimales() {
  const [animales, setAnimales]   = useState([]);
  const [cargando, setCargando]   = useState(false);
  const [error, setError]         = useState(null);

  const cargar = useCallback(async () => {
    setCargando(true);
    try {
      const data = await getAnimales();
      setAnimales(data);
      setError(null);
    } catch {
      setError('No se pudo conectar al servidor');
    } finally {
      setCargando(false);
    }
  }, []);

  useEffect(() => { cargar(); }, [cargar]);

  return { animales, cargando, error, recargar: cargar };
}

// Uso en un componente:
// const { animales, cargando, error, recargar } = useAnimales();
✏️ Ejercicio 03

Construye LoteCard

  • Crea src/components/LoteCard.jsx que muestre: código de lote, tipo de corte, peso en kg y estado.
  • Colorea el badge de estado: verde=activo, amarillo=procesado, azul=vendido, rojo=caducado.
  • Crea un hook useLotes en src/hooks/useLotes.js.
04
Navegación entre Pantallas
React Navigation — Stack, Tab y flujo completo de BioSello
⬤ Básico

Instalación

bash
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context

Flujo de navegación BioSello

src/navigation/AppNavigator.jsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useAuthStore } from '../store/authStore';

// Pantallas
import LoginScreen         from '../screens/LoginScreen';
import DashboardScreen     from '../screens/DashboardScreen';
import AnimalDetailScreen  from '../screens/AnimalDetailScreen';
import LoteFormScreen      from '../screens/LoteFormScreen';
import QRResultScreen      from '../screens/QRResultScreen';

// Tipado de rutas
export type RootStackParamList = {
  Login:        undefined;
  Tabs:         undefined;
  AnimalDetail: { id_animal };
  LoteForm:     { id_lote? };
  QRResult:     { codigo_lote };
};

const Stack = createNativeStackNavigator();
const Tab   = createBottomTabNavigator();

function MainTabs() {
  return (
    <Tab.Navigator screenOptions={{ tabBarStyle: { backgroundColor: '#161b22' }, tabBarActiveTintColor: '#3fb950' }}>
      <Tab.Screen name="Dashboard" component={DashboardScreen} options={{ tabBarLabel: 'Inicio', tabBarIcon: () => '🏠' }} />
      <Tab.Screen name="Animales" component={AnimalDetailScreen} options={{ tabBarLabel: 'Animales', tabBarIcon: () => '🐄' }} />
      <Tab.Screen name="QR" component={QRResultScreen} options={{ tabBarLabel: 'Escanear', tabBarIcon: () => '📷' }} />
    </Tab.Navigator>
  );
}

export default function AppNavigator() {
  const usuario = useAuthStore(s => s.usuario);

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#161b22' }, headerTintColor: '#e6edf3' }}>
        {!usuario ? (
          <Stack.Screen name="Login" component={LoginScreen} options={{ headerShown: false }} />
        ) : (
          <>
            <Stack.Screen name="Tabs" component={MainTabs} options={{ headerShown: false }} />
            <Stack.Screen name="AnimalDetail" component={AnimalDetailScreen} options={{ title: 'Detalle del Animal' }} />
            <Stack.Screen name="LoteForm" component={LoteFormScreen} options={{ title: 'Registrar Lote' }} />
            <Stack.Screen name="QRResult" component={QRResultScreen} options={{ title: 'Resultado QR' }} />
          </>
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Navegar y pasar parámetros

Uso en una pantalla
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../navigation/AppNavigator';

export default function AnimalRow({ animal }: { animal: Animal }) {
  const nav = useNavigation();

  return (
    <TouchableOpacity onPress={() => nav.navigate('AnimalDetail', { id_animal: animal.id_animal })}>
      <Text>{animal.num_arete}</Text>
    </TouchableOpacity>
  );
}
05
Diseño de Interfaces y Estilos
StyleSheet, Flexbox y el tema visual de BioSello
⬤ Básico

Sistema de tema — constants/theme.ts

src/constants/theme.ts
export const colors = {
  bg:       '#0d1117',
  surface:  '#161b22',
  border:   '#30363d',
  accent:   '#3fb950',   // verde BioSello
  blue:     '#58a6ff',
  danger:   '#f78166',
  gold:     '#d29922',
  text:     '#e6edf3',
  muted:    '#8b949e',
};

export const spacing = {
  xs: 4, sm: 8, md: 16, lg: 24, xl: 32,
};

export const typography = {
  h1:   { fontSize: 28, fontWeight: 'bold', color: colors.text },
  h2:   { fontSize: 22, fontWeight: '700', color: colors.text },
  body: { fontSize: 15, color: colors.text },
  mono: { fontFamily: 'monospace', fontSize: 13, color: colors.blue },
};

Flexbox en React Native

ℹ️
En React Native el eje principal por defecto es column (vertical), a diferencia de la web donde es row. Todo es Flexbox — no existe CSS Grid.
Dashboard layout con Flexbox
const styles = StyleSheet.create({
  screen:      { flex: 1, backgroundColor: colors.bg },
  header:      { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: spacing.md, borderBottomWidth: 1, borderColor: colors.border },
  statsRow:    { flexDirection: 'row', gap: 12, padding: spacing.md },
  statCard:    { flex: 1, backgroundColor: colors.surface, borderRadius: 10, padding: spacing.md, alignItems: 'center' },
  statNum:     { fontSize: 28, fontWeight: 'bold', color: colors.accent },
  statLabel:   { fontSize: 11, color: colors.muted, marginTop: 4, textAlign: 'center' },
});
✏️ Ejercicio 05

Dashboard de BioSello

  • Crea un dashboard con 3 tarjetas de estadísticas: Animales registrados, Lotes activos, Alertas pendientes.
  • Usa el sistema de tema de theme.ts en lugar de valores hardcoded.
  • Agrega un FlatList debajo con los últimos 5 lotes.
06
Formularios y Validaciones
Registro de animales, lotes y guías de tránsito
⬤ Intermedio

Formulario — Nuevo Animal

src/screens/NuevoAnimalScreen.jsx
import React, { useState } from 'react';
import { View, TextInput, Text, ScrollView, TouchableOpacity, StyleSheet } from 'react-native';
import { colors, spacing } from '../constants/theme';
import { crearAnimal } from '../api/animales';

const ESPECIES = ['BOVINO', 'PORCINO', 'OVINO', 'CAPRINO', 'EQUINO'];
const SEXOS    = ['MACHO', 'HEMBRA'];

export default function NuevoAnimalScreen() {
  const [form, setForm] = useState({
    num_arete:     '',
    especie:       'BOVINO',
    sexo:          'HEMBRA',
    meses_edad:    '',
    arete_faltante: false,
  });
  const [errores, setErrores] = useState({});

  const validar = () => {
    const e = {};
    if (!form.num_arete || form.num_arete.length < 10) e.num_arete = 'Arete inválido (mín. 10 dígitos)';
    if (!form.meses_edad || isNaN(Number(form.meses_edad))) e.meses_edad = 'Edad requerida';
    return e;
  };

  const guardar = async () => {
    const e = validar();
    if (Object.keys(e).length) { setErrores(e); return; }
    await crearAnimal({ ...form, meses_edad: Number(form.meses_edad) });
  };

  return (
    <ScrollView style={s.screen}>
      <Text style={s.label}>Número de Arete *</Text>
      <TextInput style={[s.input, errores.num_arete && s.inputErr]} value={form.num_arete}
        onChangeText={v => setForm(f => ({...f, num_arete: v}))} placeholder="1301226566" placeholderTextColor={colors.muted} keyboardType="numeric" />
      {errores.num_arete && <Text style={s.err}>{errores.num_arete}</Text>}

      <Text style={s.label}>Especie</Text>
      <View style={s.chips}>
        {ESPECIES.map(esp => (
          <TouchableOpacity key={esp} style={[s.chip, form.especie===esp && s.chipActive]} onPress={() => setForm(f => ({...f, especie:esp}))}>
            <Text style={form.especie===esp ? s.chipTxtActive : s.chipTxt}>{esp}</Text>
          </TouchableOpacity>
        ))}
      </View>

      <TouchableOpacity style={s.btn} onPress={guardar}>
        <Text style={s.btnTxt}>Registrar Animal</Text>
      </TouchableOpacity>
    </ScrollView>
  );
}

const s = StyleSheet.create({
  screen:       { flex: 1, backgroundColor: colors.bg, padding: spacing.md },
  label:        { color: colors.muted, fontSize: 12, fontWeight: '700', textTransform: 'uppercase', letterSpacing: 0.8, marginTop: spacing.md, marginBottom: 6 },
  input:        { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 8, padding: spacing.md, color: colors.text, fontSize: 15 },
  inputErr:     { borderColor: colors.danger },
  err:          { color: colors.danger, fontSize: 12, marginTop: 4 },
  chips:        { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 8 },
  chip:         { paddingHorizontal: 12, paddingVertical: 7, borderRadius: 20, borderWidth: 1, borderColor: colors.border },
  chipActive:   { backgroundColor: colors.accent, borderColor: colors.accent },
  chipTxt:      { color: colors.muted, fontSize: 13 },
  chipTxtActive:{ color: '#000', fontWeight: '700', fontSize: 13 },
  btn:          { backgroundColor: colors.accent, borderRadius: 10, padding: spacing.md, alignItems: 'center', marginTop: spacing.lg },
  btnTxt:       { color: '#000', fontWeight: '700', fontSize: 16 },
});
07
Consumo de APIs
Axios, manejo de errores y tipado de respuestas del backend BioSello
⬤ Intermedio

Cliente Axios configurado

src/api/client.ts
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const api = axios.create({
  baseURL: __DEV__ ? 'http://192.168.1.100:3000/api' : 'https://biosello.app/api',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
});

// Interceptor: agrega JWT a cada petición
api.interceptors.request.use(async config => {
  const token = await AsyncStorage.getItem('biosello_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Interceptor: redirige al login si el token expiró
api.interceptors.response.use(
  r => r,
  async err => {
    if (err.response?.status === 401) {
      await AsyncStorage.removeItem('biosello_token');
      // navegar al login...
    }
    return Promise.reject(err);
  }
);

Módulo de animales

src/api/animales.ts
import { api } from './client';
import { Animal } from '../constants/shapes';

export const getAnimales = async () => {
  const { data } = await api.get('/animales');
  return data;
};

export const getAnimalByArete = async (num_arete): Promise<Animal> => {
  const { data } = await api.get(`/animales/arete/${num_arete}`);
  return data;
};

export const crearAnimal = async (payload): Promise<Animal> => {
  const { data } = await api.post('/animales', payload);
  return data;
};
08
Manejo de Almacenamiento Local
AsyncStorage y Zustand para estado global persistente
⬤ Intermedio

Zustand — Auth Store

src/store/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Usuario } from '../constants/shapes';

export const useAuthStore = create()(
  persist(
    (set) => ({
      usuario: null,
      token:   null,
      login:   (usuario, token) => set({ usuario, token }),
      logout:  () => set({ usuario: null, token: null }),
    }),
    {
      name:    'biosello-auth',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);
Ventaja: Con persist, el usuario permanece logueado aunque cierre la app. El token se guarda automáticamente en AsyncStorage.
09
Arquitectura y Buenas Prácticas
Patrones clean, separación de responsabilidades y mantenibilidad
⬤ Intermedio

Capas de la aplicación

CapaResponsabilidadArchivos BioSello
PresentaciónRenderizar UI, recibir eventos del usuarioscreens/, components/
DominioLógica de negocio, validaciones, tiposhooks/, utils/, types/
DatosLlamadas HTTP, caché, persistenciaapi/, store/

Reglas de oro

  • Un archivo = una responsabilidad. AnimalCard.tsx solo renderiza un animal.
  • No lógica de negocio en las pantallas. Mueve validaciones a utils/validators.ts.
  • Nunca hagas fetch directamente en un componente. Usa hooks (useAnimales) o el módulo api/.
  • Documenta con JSDoc. Cada objeto de respuesta del servidor debe tener su typedef en constants/shapes.js.
  • Valida en runtime. Sin compilador, verifica los datos que llegan del servidor antes de usarlos.

Validadores reutilizables

src/utils/validators.ts
export const validarArete = (arete) => {
  if (!/^\d{10,13}$/.test(arete)) return 'El arete debe tener 10–13 dígitos numéricos';
  return null;
};

export const validarFolio = (folio) => {
  if (!folio.trim()) return 'El folio de guía es obligatorio';
  if (folio.length > 20) return 'Máximo 20 caracteres';
  return null;
};
10
Integración con MariaDB
Conexión, schema biosello y consultas con mysql2
⬤ Avanzado

Schema de la BD biosello

animal
id_animalPK
num_aretevarchar(30)
especieenum
sexoenum
clasificacionenum
meses_edadsmallint
id_origenFK
id_propietarioFK
lote
id_lotePK
codigo_lotevarchar(50)
tipo_cortevarchar
peso_kgdecimal
fecha_vencimientodate
estadoenum
id_animalFK
guia_transito
id_guiaPK
folio_guiavarchar(20)
motivo_movilizacionenum
fecha_expediciondate
fecha_vencimientodate
id_propietarioFK
id_rastroFK
usuario
id_usuarioPK
nombrevarchar
emailvarchar
contrasena_hashvarchar
perfilenum
codigo_qr
id_qrPK
id_loteFK
url_destinovarchar
fecha_generadodatetime
alerta
id_alertaPK
tipoenum
id_loteFK
id_usuario_destinoFK
leidatinyint

Pool de conexión (Node.js)

backend/src/db.js
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();

export const pool = mysql.createPool({
  host:     process.env.DB_HOST     ?? 'localhost',
  user:     process.env.DB_USER     ?? 'root',
  password: process.env.DB_PASSWORD ?? '',
  database: process.env.DB_NAME     ?? 'biosello',
  port:     Number(process.env.DB_PORT) || 3306,
  waitForConnections: true,
  connectionLimit:    10,
  charset:            'utf8',
});

// Función helper
export async function query(sql, values?[]) {
  const [rows] = await pool.execute(sql, values);
  return rows;
}
11
Desarrollo del Backend — API REST
Node.js + Express: endpoints para BioSello
⬤ Avanzado

Estructura del backend

backend/
backend/
├── src/
│   ├── db.js
│   ├── app.js
│   ├── routes/
│   │   ├── auth.routes.js
│   │   ├── animales.routes.js
│   │   ├── lotes.routes.js
│   │   ├── guias.routes.js
│   │   └── qr.routes.js
│   ├── controllers/
│   │   ├── animales.controller.js
│   │   └── lotes.controller.js
│   └── middleware/
│       └── auth.middleware.js
├── .env
└── package.json

app.js — Servidor Express

backend/src/app.js
import express from 'express';
import cors from 'cors';
import authRoutes    from './routes/auth.routes';
import animalRoutes  from './routes/animales.routes';
import loteRoutes    from './routes/lotes.routes';
import guiaRoutes    from './routes/guias.routes';
import qrRoutes      from './routes/qr.routes';

const app = express();
app.use(cors());
app.use(express.json());

app.use('/api/auth',     authRoutes);
app.use('/api/animales', animalRoutes);
app.use('/api/lotes',    loteRoutes);
app.use('/api/guias',    guiaRoutes);
app.use('/api/qr',       qrRoutes);

app.listen(3000, () => console.log('🐄 BioSello API corriendo en :3000'));

Controlador de Animales

backend/src/controllers/animales.controller.js
import { Request, Response } from 'express';
import { query } from '../db';
import { Animal } from '../constants/shapes';

export const getAnimales = async (req, res) => {
  const animales = await query(
    `SELECT a.*, p.nombre_propietario, o.localidad_origen
       FROM animal a
       JOIN propietario p ON p.id_propietario = a.id_propietario
       JOIN origen o      ON o.id_origen = a.id_origen
      ORDER BY a.created_at DESC`
  );
  res.json(animales);
};

export const getAnimalByArete = async (req, res) => {
  const { num_arete } = req.params;
  const [animal] = await query(
    'SELECT * FROM animal WHERE num_arete = ?', [num_arete]
  );
  if (!animal) return res.status(404).json({ error: 'Animal no encontrado' });
  res.json(animal);
};

export const crearAnimal = async (req, res) => {
  const { num_arete, especie, sexo, clasificacion, meses_edad, id_origen, id_propietario } = req.body;
  const [result] = await query(
    'INSERT INTO animal (num_arete,especie,sexo,clasificacion,meses_edad,id_origen,id_propietario) VALUES (?,?,?,?,?,?,?)',
    [num_arete, especie, sexo, clasificacion, meses_edad, id_origen, id_propietario]
  );
  res.status(201).json({ id_animal: result.insertId });
};

Tabla de endpoints

MétodoRutaDescripciónAuth
POST/api/auth/loginLogin con email/password
GET/api/animalesListar todos los animalesJWT
GET/api/animales/arete/:numBuscar animal por areteJWT
POST/api/animalesRegistrar animalJWT admin/empleado
GET/api/lotesListar lotesJWT
POST/api/lotesCrear loteJWT empleado
GET/api/lotes/:id/qrGenerar QR del loteJWT
GET/api/guiasListar guías de tránsitoJWT
GET/api/qr/scan/:codigoDatos públicos del lote (sin auth)
12
Autenticación y Control de Acceso
JWT, bcrypt, roles y middleware de autorización
⬤ Avanzado

Login con bcrypt + JWT

backend/src/routes/auth.routes.js
import express from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { query } from '../db';

const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET ?? 'biosello_secret';

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const [usuario] = await query(
    'SELECT * FROM usuario WHERE email = ? AND activo = 1', [email]
  );
  if (!usuario) return res.status(401).json({ error: 'Credenciales inválidas' });

  const ok = await bcrypt.compare(password, usuario.contrasena_hash);
  if (!ok) return res.status(401).json({ error: 'Credenciales inválidas' });

  const token = jwt.sign(
    { id_usuario: usuario.id_usuario, perfil: usuario.perfil },
    JWT_SECRET, { expiresIn: '8h' }
  );

  // Guardar sesión en BD
  await query(
    'INSERT INTO sesion (id_usuario, token, fecha_expira) VALUES (?,?,DATE_ADD(NOW(),INTERVAL 8 HOUR))',
    [usuario.id_usuario, token]
  );

  res.json({ token, usuario: { id_usuario: usuario.id_usuario, nombre: usuario.nombre, perfil: usuario.perfil } });
});

export default router;

Middleware de autenticación

backend/src/middleware/auth.middleware.js
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET ?? 'biosello_secret';

// Requiere JWT válido
export const auth = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Token requerido' });
  try {
    req.user = jwt.verify(token, JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Token inválido' });
  }
};

// Requiere rol específico
export const requireRole = (...roles[]) =>
  (req, res, next) => {
    const user = req.user;
    if (!roles.includes(user?.perfil))
      return res.status(403).json({ error: 'Sin permiso' });
    next();
  };

// Uso: router.post('/animales', auth, requireRole('admin','empleado'), crearAnimal);
13
Generación y Lectura de Códigos QR
QR por lote, escaneo con cámara y trazabilidad completa
⬤ Avanzado

Generar QR en el backend

backend/src/routes/qr.routes.js
import express from 'express';
import QRCode from 'qrcode';
import { query } from '../db';
import { auth } from '../middleware/auth.middleware';

const router = express.Router();

// Genera QR para un lote y lo guarda en BD
router.post('/generar/:id_lote', auth, async (req, res) => {
  const { id_lote } = req.params;
  const url = `https://biosello.app/scan/${id_lote}`;

  const qrBase64 = await QRCode.toDataURL(url, { errorCorrectionLevel: 'H', width: 400 });

  const user = req.user;
  const [result] = await query(
    'INSERT INTO codigo_qr (id_lote, url_destino, generado_por) VALUES (?,?,?)',
    [id_lote, url, user.id_usuario]
  );
  res.json({ id_qr: result.insertId, url, qrBase64 });
});

// Endpoint público para cuando alguien escanea el QR
router.get('/scan/:id_lote', async (req, res) => {
  const { id_lote } = req.params;

  // Registrar el escaneo
  const [qr] = await query('SELECT id_qr FROM codigo_qr WHERE id_lote=? ORDER BY fecha_generado DESC LIMIT 1',[id_lote]);
  if (qr) await query('INSERT INTO escaneo(id_qr,ip,dispositivo) VALUES(?,?,?)',[qr.id_qr, req.ip, req.headers['user-agent']]);

  const [lote] = await query(
    `SELECT l.*, a.num_arete, a.especie, a.clasificacion, p.nombre_propietario, r.nombre_rastro
       FROM lote l
  LEFT JOIN animal a ON a.id_animal = l.id_animal
  LEFT JOIN propietario p ON p.id_propietario = a.id_propietario
  LEFT JOIN guia_animal ga ON ga.id_animal = a.id_animal
  LEFT JOIN guia_transito gt ON gt.id_guia = ga.id_guia
  LEFT JOIN rastro r ON r.id_rastro = gt.id_rastro
      WHERE l.id_lote = ?`, [id_lote]
  );
  if (!lote) return res.status(404).json({ error: 'Lote no encontrado' });
  res.json(lote);
});

export default router;

Escanear QR en React Native

src/screens/QRScannerScreen.jsx
// npm install react-native-vision-camera
import React, { useState } from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import { Camera, useCameraDevices, useCodeScanner } from 'react-native-vision-camera';
import { useNavigation } from '@react-navigation/native';

export default function QRScannerScreen() {
  const devices = useCameraDevices();
  const device  = devices.back;
  const nav     = useNavigation();
  const [scanned, setScanned] = useState(false);

  const codeScanner = useCodeScanner({
    codeTypes: ['qr'],
    onCodeScanned: (codes) => {
      if (scanned || !codes[0]?.value) return;
      setScanned(true);
      const url = codes[0].value;
      const id_lote = url.split('/').pop();
      nav.navigate('QRResult', { codigo_lote: id_lote });
    },
  });

  if (!device) return <Text>Cargando cámara…</Text>;

  return (
    <View style={s.container}>
      <Camera style={StyleSheet.absoluteFill} device={device} isActive={!scanned} codeScanner={codeScanner} />
      <View style={s.overlay}>
        <View style={s.frame} />
        <Text style={s.hint}>Apunta al código QR del lote</Text>
        {scanned && <TouchableOpacity style={s.btn} onPress={() => setScanned(false)}><Text>Escanear otro</Text></TouchableOpacity>}
      </View>
    </View>
  );
}

const s = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#000' },
  overlay:   { ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center' },
  frame:     { width: 240, height: 240, borderWidth: 2, borderColor: '#3fb950', borderRadius: 12 },
  hint:      { color: '#fff', marginTop: 20, fontSize: 14 },
  btn:       { marginTop: 20, padding: 12, backgroundColor: '#3fb950', borderRadius: 8 },
});
14
Realidad Aumentada en React Native
ViroReact / React Native VisionCamera + Skia para overlay de info
⬤ Avanzado
⚠️
Complejidad alta. La RA en React Native requiere compilación nativa. Considera dos enfoques: ViroReact (AR 3D completo) o VisionCamera + canvas overlay (más sencillo y confiable para datos textuales).

Enfoque 1: Canvas Overlay (Recomendado para BioSello)

Usa react-native-vision-camera con react-native-skia para dibujar información del lote sobre la cámara en tiempo real, cuando detectas el QR.

bash — Instalación
npm install react-native-vision-camera @shopify/react-native-skia
npm install vision-camera-code-scanner
src/screens/ARLoteScreen.jsx
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Camera, useCameraDevices } from 'react-native-vision-camera';
import { Canvas, RoundedRect, Text, Fill, useFont } from '@shopify/react-native-skia';
import { Lote } from '../constants/shapes';

export default function ARLoteScreen({ lote }) {
  const devices = useCameraDevices();
  const device  = devices.back;
  const font    = useFont(require('../assets/fonts/Inter-Bold.ttf'), 14);

  if (!device) return null;

  return (
    <View style={s.container}>
      <Camera style={StyleSheet.absoluteFill} device={device} isActive={true} />

      {lote && (
        <Canvas style={StyleSheet.absoluteFill}>
          <{/* Panel semi-transparente */}
          <RoundedRect x={20} y={100} width={300} height={160} r={12} color="rgba(13,17,23,0.85)" />

          <{/* Datos del lote sobre la cámara */}
          <Text x={36} y={130} text={`🏷 ${lote.codigo_lote}`} font={font} color="#3fb950" />
          <Text x={36} y={155} text={`Corte: ${lote.tipo_corte}`} font={font} color="#e6edf3" />
          <Text x={36} y={180} text={`Peso: ${lote.peso_kg} kg`} font={font} color="#e6edf3" />
          <Text x={36} y={205} text={`Vence: ${lote.fecha_vencimiento}`} font={font}
            color={lote.estado === 'caducado' ? '#f78166' : '#3fb950'} />
          <Text x={36} y={230} text={`Estado: ${lote.estado.toUpperCase()}`} font={font} color="#8b949e" />
        </Canvas>
      )}
    </View>
  );
}

const s = StyleSheet.create({ container: { flex: 1 } });

Enfoque 2: ViroReact (AR 3D)

bash
npm install @viro-community/react-viro
Escena AR con objeto 3D sobre lote
import { ViroARScene, ViroText, ViroNode, ViroARImageMarker, ViroARTrackingTargets } from '@viro-community/react-viro';

// Registrar la imagen del QR como marker
ViroARTrackingTargets.createTargets({
  bioselloQR: { source: require('../assets/qr_target.png'), orientation: 'Up', physicalWidth: 0.1 },
});

export function ARLoteScene({ lote }) {
  return (
    <ViroARScene>
      <ViroARImageMarker target="bioselloQR">
        <ViroNode position={[0, 0.1, 0]}>
          <ViroText text={`${lote.tipo_corte}\n${lote.peso_kg} kg\n${lote.estado}`}
            position={[0, 0, 0]} scale={[0.5, 0.5, 0.5]}
            style={{ fontFamily: 'Arial', fontSize: 20, color: '#3fb950' }} />
        </ViroNode>
      </ViroARImageMarker>
    </ViroARScene>
  );
}
15
Despliegue, Pruebas y Generación del APK
Build de producción, firma y distribución de BioSello
⬤ Avanzado

1. Generar keystore de firma

bash — Solo se hace una vez
keytool -genkeypair -v \
  -keystore android/app/biosello-release.keystore \
  -alias biosello \
  -keyalg RSA -keysize 2048 \
  -validity 10000

2. Configurar gradle

android/gradle.properties
BIOSELLO_UPLOAD_STORE_FILE=biosello-release.keystore
BIOSELLO_UPLOAD_KEY_ALIAS=biosello
BIOSELLO_UPLOAD_STORE_PASSWORD=tu_password
BIOSELLO_UPLOAD_KEY_PASSWORD=tu_password
android/app/build.gradle
android {
  ...
  signingConfigs {
    release {
      storeFile     file(BIOSELLO_UPLOAD_STORE_FILE)
      storePassword BIOSELLO_UPLOAD_STORE_PASSWORD
      keyAlias      BIOSELLO_UPLOAD_KEY_ALIAS
      keyPassword   BIOSELLO_UPLOAD_KEY_PASSWORD
    }
  }
  buildTypes {
    release {
      signingConfig signingConfigs.release
      minifyEnabled enableProguardInReleaseBuilds
      shrinkResources enableProguardInReleaseBuilds
    }
  }
}

3. Generar el APK / AAB

bash
# APK para distribución directa
cd android && ./gradlew assembleRelease

# AAB para Google Play (recomendado)
cd android && ./gradlew bundleRelease

# El APK queda en:
# android/app/build/outputs/apk/release/app-release.apk

Checklist antes de publicar BioSello

ÍtemComando / AcciónEstado
Sin errores ESLintnpx eslint src/
Tests unitariosnpx jest
Variables de entorno prod.env.production con URL real
Permisos AndroidCAMERA, INTERNET en AndroidManifest
Icono y splash screennpx react-native-asset
versionCode incrementadobuild.gradle
ProGuard activadominifyEnabled true
SSL en el backendHTTPS con certificado válido
Distribución interna: Para el rastro de Huejutla y los inspectores, puedes compartir el APK directamente por WhatsApp o subirlo a Firebase App Distribution sin necesitar Google Play.