21- Tutorial React Native Paso a Paso - Consumir API Laravel con Token


馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)

bash
# 1.1 Crear proyecto React Native con Expo
npx create-expo-app GestorTareasTokenRN
cd GestorTareasTokenRN

# 1.2 Instalar dependencias necesarias
npm install
npm install @react-native-async-storage/async-storage

# 1.3 Iniciar el proyecto
npx expo start

馃搧 Paso 2: Estructura de Carpetas (2 minutos)

text
GestorTareasTokenRN/
├── src/
│   ├── components/
│   │   ├── TareaList.js
│   │   ├── TareaForm.js
│   │   └── TareaItem.js
│   ├── services/
│   │   └── tareaService.js
│   ├── utils/
│   │   └── tokenManager.js
│   └── App.js
├── App.js
└── package.json

馃摝 Paso 3: App.js principal (3 minutos)

jsx
// App.js
import React from 'react';
import { SafeAreaView, StatusBar, StyleSheet } from 'react-native';
import TareaList from './src/components/TareaList';

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <StatusBar 
        barStyle="dark-content"
        backgroundColor="#ffffff"
      />
      <TareaList />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
});

馃攽 Paso 4: Gestor de Token con AsyncStorage (10 minutos)

jsx
// src/utils/tokenManager.js
import AsyncStorage from '@react-native-async-storage/async-storage';

class TokenManager {
  constructor() {
    this.tokenKey = 'api_token';
  }

  // Obtener token de AsyncStorage
  async getToken() {
    try {
      return await AsyncStorage.getItem(this.tokenKey);
    } catch (error) {
      console.error('Error al obtener token:', error);
      return null;
    }
  }

  // Guardar token en AsyncStorage
  async saveToken(token) {
    try {
      await AsyncStorage.setItem(this.tokenKey, token);
    } catch (error) {
      console.error('Error al guardar token:', error);
    }
  }

  // Eliminar token
  async clearToken() {
    try {
      await AsyncStorage.removeItem(this.tokenKey);
    } catch (error) {
      console.error('Error al eliminar token:', error);
    }
  }

  // Verificar si hay token
  async hasToken() {
    const token = await this.getToken();
    return !!token;
  }

  // Generar nuevo token desde la API
  async generarNuevoToken() {
    try {
      console.log('Generando nuevo token...');
      
      const response = await fetch('http://192.168.1.100:8000/api/token/generar', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
      });
      
      console.log('Respuesta recibida:', response.status);
      
      if (!response.ok) {
        const errorText = await response.text();
        console.error('Error en respuesta:', errorText);
        throw new Error(`Error ${response.status}: No se pudo generar token`);
      }
      
      const data = await response.json();
      console.log('Token generado:', data.token);
      
      // Guardar el token
      await this.saveToken(data.token);
      
      return {
        success: true,
        token: data.token,
        message: 'Token generado y guardado'
      };
      
    } catch (error) {
      console.error('Error al generar token:', error.message);
      return {
        success: false,
        message: error.message
      };
    }
  }

  // Forzar generaci贸n de token si no existe
  async asegurarToken() {
    const tieneToken = await this.hasToken();
    
    if (!tieneToken) {
      console.log('No hay token, generando uno nuevo...');
      return await this.generarNuevoToken();
    }
    
    const token = await this.getToken();
    return { success: true, token };
  }
}

// Crear una instancia 煤nica
const tokenManager = new TokenManager();
export default tokenManager;

馃攲 Paso 5: Servicio de API con Token (15 minutos)

jsx
// src/services/tareaService.js
import tokenManager from '../utils/tokenManager';

const API_URL = 'http://192.168.1.100:8000/api';

class TareaService {
  // Configuraci贸n com煤n para todas las peticiones
  async configurarPeticion(metodo, datos = null) {
    // Asegurar que tenemos un token
    const tokenResult = await tokenManager.asegurarToken();
    
    if (!tokenResult.success) {
      throw new Error('No se pudo obtener token: ' + tokenResult.message);
    }

    const token = tokenResult.token;
    
    // Configurar headers
    const config = {
      method: metodo,
      headers: {
        'Content-Type': 'application/json',
        'Token': token,
        'Accept': 'application/json'
      }
    };

    // Agregar body si hay datos
    if (datos) {
      config.body = JSON.stringify(datos);
    }

    return config;
  }

  // 1. Obtener todas las tareas
  async obtenerTodas() {
    try {
      console.log('Obteniendo tareas...');
      const config = await this.configurarPeticion('GET');
      
      const respuesta = await fetch(`${API_URL}/tareas`, config);
      console.log('Status respuesta:', respuesta.status);
      
      if (respuesta.status === 401) {
        console.log('Token inv谩lido, generando nuevo...');
        await tokenManager.generarNuevoToken();
        return await this.obtenerTodas(); // Reintentar
      }
      
      if (!respuesta.ok) {
        const errorText = await respuesta.text();
        console.error('Error en respuesta:', errorText);
        throw new Error(`Error ${respuesta.status}: No se pudieron obtener tareas`);
      }
      
      const data = await respuesta.json();
      console.log('Tareas obtenidas:', data.length || 0);
      return data.tareas || data;
      
    } catch (error) {
      console.error('Error en obtenerTodas:', error.message);
      throw error;
    }
  }

  // 2. Crear nueva tarea
  async crearTarea(tarea) {
    try {
      console.log('Creando tarea:', tarea.titulo);
      const config = await this.configurarPeticion('POST', tarea);
      
      const respuesta = await fetch(`${API_URL}/tareas`, config);
      
      if (respuesta.status === 401) {
        await tokenManager.generarNuevoToken();
        return await this.crearTarea(tarea);
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al crear tarea');
      }
      
      return await respuesta.json();
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  }

  // 3. Actualizar tarea (solo para completar)
  async actualizarTarea(id, datos) {
    try {
      const config = await this.configurarPeticion('PUT', datos);
      
      const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
      
      if (respuesta.status === 401) {
        await tokenManager.generarNuevoToken();
        return await this.actualizarTarea(id, datos);
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al actualizar tarea');
      }
      
      return await respuesta.json();
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  }

  // 4. Eliminar tarea
  async eliminarTarea(id) {
    try {
      const config = await this.configurarPeticion('DELETE');
      
      const respuesta = await fetch(`${API_URL}/tareas/${id}`, config);
      
      if (respuesta.status === 401) {
        await tokenManager.generarNuevoToken();
        return await this.eliminarTarea(id);
      }
      
      if (!respuesta.ok) {
        throw new Error('Error al eliminar tarea');
      }
      
      return await respuesta.json();
      
    } catch (error) {
      console.error('Error:', error);
      throw error;
    }
  }

  // 5. Obtener token actual
  async getTokenActual() {
    return await tokenManager.getToken();
  }

  // 6. Generar nuevo token manualmente
  async generarNuevoTokenManual() {
    return await tokenManager.generarNuevoToken();
  }

  // 7. Limpiar token
  async limpiarToken() {
    await tokenManager.clearToken();
  }
}

// Crear una instancia 煤nica
const tareaService = new TareaService();
export default tareaService;

馃搵 Paso 6: TareaList.js con Token (20 minutos)

jsx
// src/components/TareaList.js
import React, { useState, useEffect } from 'react';
import { 
  View, 
  Text, 
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  ActivityIndicator,
  Alert,
  useWindowDimensions
} from 'react-native';
import TareaForm from './TareaForm';
import TareaItem from './TareaItem';
import tareaService from '../services/tareaService';

function TareaList() {
  const [tareas, setTareas] = useState([]);
  const [cargando, setCargando] = useState(true);
  const [error, setError] = useState('');
  const [tokenInfo, setTokenInfo] = useState('');
  const [regenerandoToken, setRegenerandoToken] = useState(false);
  
  const { width } = useWindowDimensions();
  const esTablet = width >= 768;

  // Cargar tareas al iniciar
  useEffect(() => {
    cargarTareas();
    actualizarTokenInfo();
  }, []);

  // Mostrar informaci贸n del token
  const actualizarTokenInfo = async () => {
    try {
      const token = await tareaService.getTokenActual();
      setTokenInfo(token ? `Token: ${token}` : 'No hay token');
    } catch (error) {
      console.error('Error al obtener token info:', error);
    }
  };

  // Funci贸n para cargar tareas
  const cargarTareas = async () => {
    try {
      setCargando(true);
      setError('');
      console.log('Iniciando carga de tareas...');
      
      const datos = await tareaService.obtenerTodas();
      console.log('Datos recibidos:', datos);
      
      setTareas(datos);
      await actualizarTokenInfo();
      
    } catch (error) {
      console.error('Error en cargarTareas:', error.message);
      setError(`❌ Error: ${error.message}`);
    } finally {
      setCargando(false);
    }
  };

  // Funci贸n para crear tarea
  const manejarCrearTarea = async (nuevaTarea) => {
    try {
      await tareaService.crearTarea(nuevaTarea);
      await cargarTareas();
      Alert.alert('✅ 脡xito', 'Tarea creada correctamente');
    } catch (error) {
      Alert.alert('❌ Error', 'Error al crear la tarea: ' + error.message);
    }
  };

  // Funci贸n para eliminar tarea
  const manejarEliminarTarea = async (id) => {
    Alert.alert(
      'Eliminar Tarea',
      '¿Est谩s seguro de eliminar esta tarea?',
      [
        { text: 'Cancelar', style: 'cancel' },
        { 
          text: 'Eliminar', 
          onPress: async () => {
            try {
              await tareaService.eliminarTarea(id);
              await cargarTareas();
              Alert.alert('✅ 脡xito', 'Tarea eliminada correctamente');
            } catch (error) {
              Alert.alert('❌ Error', 'Error al eliminar la tarea: ' + error.message);
            }
          }, 
          style: 'destructive' 
        },
      ]
    );
  };

  // Funci贸n para completar tarea
  const manejarCompletarTarea = async (id, completada) => {
    try {
      await tareaService.actualizarTarea(id, { completada });
      await cargarTareas();
    } catch (error) {
      Alert.alert('❌ Error', 'Error al actualizar la tarea: ' + error.message);
    }
  };

  // Funci贸n para generar nuevo token
  const generarNuevoToken = async () => {
    try {
      setRegenerandoToken(true);
      const resultado = await tareaService.generarNuevoTokenManual();
      
      if (resultado.success) {
        Alert.alert('✅ Token Generado', `Nuevo token: ${resultado.token}`);
        await actualizarTokenInfo();
        await cargarTareas();
      } else {
        Alert.alert('❌ Error', resultado.message);
      }
    } catch (error) {
      Alert.alert('❌ Error', 'Error al generar token: ' + error.message);
    } finally {
      setRegenerandoToken(false);
    }
  };

  return (
    <View style={styles.container}>
      {/* Encabezado */}
      <View style={styles.header}>
        <Text style={styles.tituloPrincipal}>馃攼 Gestor de Tareas con Token</Text>
        <Text style={styles.subtituloPrincipal}>
          API Laravel con autenticaci贸n por token
        </Text>
      </View>

      {/* Panel de Token */}
      <View style={styles.tokenPanel}>
        <Text style={styles.tokenTitulo}>馃攽 Autenticaci贸n por Token</Text>
        <Text style={styles.tokenInfo}>{tokenInfo}</Text>
        
        <TouchableOpacity 
          style={styles.botonToken}
          onPress={generarNuevoToken}
          disabled={regenerandoToken}
        >
          {regenerandoToken ? (
            <ActivityIndicator size="small" color="#ffffff" />
          ) : (
            <Text style={styles.botonTokenTexto}>馃攧 Generar Nuevo Token</Text>
          )}
        </TouchableOpacity>
      </View>

      {/* Contenido principal */}
      <ScrollView 
        style={styles.scrollView}
        contentContainerStyle={styles.scrollViewContent}
      >
        <View style={[
          styles.contenido,
          esTablet && styles.contenidoTablet
        ]}>
          {/* Columna izquierda - Formulario */}
          <View style={[
            styles.columnaIzquierda,
            esTablet && styles.columnaIzquierdaTablet
          ]}>
            <Text style={styles.tituloSeccion}>➕ Nueva Tarea</Text>
            <TareaForm onSubmit={manejarCrearTarea} />
            
            <TouchableOpacity 
              style={styles.botonRecargar}
              onPress={cargarTareas}
              disabled={cargando}
            >
              {cargando ? (
                <ActivityIndicator size="small" color="#ffffff" />
              ) : (
                <Text style={styles.botonRecargarTexto}>馃攧 Recargar Tareas</Text>
              )}
            </TouchableOpacity>
          </View>

          {/* Columna derecha - Lista */}
          <View style={[
            styles.columnaDerecha,
            esTablet && styles.columnaDerechaTablet
          ]}>
            <View style={styles.headerLista}>
              <Text style={styles.tituloLista}>馃搵 Lista de Tareas</Text>
            </View>
            
            {error ? (
              <View style={styles.errorContainer}>
                <Text style={styles.errorTexto}>{error}</Text>
                <TouchableOpacity 
                  style={styles.botonError}
                  onPress={cargarTareas}
                >
                  <Text style={styles.botonErrorTexto}>Reintentar</Text>
                </TouchableOpacity>
              </View>
            ) : cargando ? (
              <View style={styles.cargandoContainer}>
                <ActivityIndicator size="large" color="#4CAF50" />
                <Text style={styles.cargandoTexto}>
                  Cargando tareas desde API...
                </Text>
                <Text style={styles.cargandoSubtexto}>
                  (Se est谩 generando/verificando el token autom谩ticamente)
                </Text>
              </View>
            ) : tareas.length === 0 ? (
              <View style={styles.vacioContainer}>
                <Text style={styles.vacioTexto}>馃摥 No hay tareas</Text>
                <Text style={styles.vacioSubtexto}>
                  ¡Crea tu primera tarea usando el formulario!
                </Text>
              </View>
            ) : (
              <>
                <View style={styles.infoContainer}>
                  <Text style={styles.infoTexto}>
                    ✅ Conectado a la API
                  </Text>
                  <Text style={styles.infoSubtexto}>
                    Total: {tareas.length} tareas | Token activo
                  </Text>
                </View>
                
                {tareas.map((tarea) => (
                  <TareaItem
                    key={tarea.id}
                    tarea={tarea}
                    onEliminar={manejarEliminarTarea}
                    onCompletar={manejarCompletarTarea}
                  />
                ))}
              </>
            )}
          </View>
        </View>
      </ScrollView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  header: {
    backgroundColor: '#ffffff',
    padding: 20,
    alignItems: 'center',
    borderBottomWidth: 1,
    borderBottomColor: '#e9ecef',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.05,
    shadowRadius: 3,
    elevation: 3,
  },
  tituloPrincipal: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#333',
    textAlign: 'center',
  },
  subtituloPrincipal: {
    fontSize: 14,
    color: '#666',
    marginTop: 5,
    textAlign: 'center',
  },
  tokenPanel: {
    backgroundColor: '#e3f2fd',
    padding: 15,
    margin: 15,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#bbdefb',
  },
  tokenTitulo: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 5,
    color: '#1565c0',
  },
  tokenInfo: {
    fontSize: 14,
    color: '#1976d2',
    marginBottom: 10,
  },
  botonToken: {
    backgroundColor: '#2196F3',
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
  },
  botonTokenTexto: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 14,
  },
  scrollView: {
    flex: 1,
  },
  scrollViewContent: {
    paddingBottom: 20,
  },
  contenido: {
    paddingHorizontal: 15,
  },
  contenidoTablet: {
    flexDirection: 'row',
  },
  columnaIzquierda: {
    marginBottom: 20,
  },
  columnaIzquierdaTablet: {
    flex: 1,
    paddingRight: 10,
    marginBottom: 0,
  },
  columnaDerechaTablet: {
    flex: 1,
    paddingLeft: 10,
  },
  tituloSeccion: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 15,
    color: '#333',
  },
  botonRecargar: {
    backgroundColor: '#4CAF50',
    padding: 12,
    borderRadius: 6,
    marginTop: 15,
    alignItems: 'center',
  },
  botonRecargarTexto: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 15,
  },
  headerLista: {
    marginBottom: 15,
    paddingBottom: 10,
    borderBottomWidth: 1,
    borderBottomColor: '#dee2e6',
  },
  tituloLista: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333',
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    padding: 20,
    borderRadius: 10,
    alignItems: 'center',
    borderWidth: 1,
    borderColor: '#f5c6cb',
  },
  errorTexto: {
    color: '#721c24',
    textAlign: 'center',
    marginBottom: 15,
  },
  botonError: {
    backgroundColor: '#dc3545',
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 5,
  },
  botonErrorTexto: {
    color: 'white',
    fontWeight: 'bold',
  },
  cargandoContainer: {
    padding: 40,
    alignItems: 'center',
  },
  cargandoTexto: {
    fontSize: 16,
    color: '#666',
    marginTop: 15,
    marginBottom: 5,
  },
  cargandoSubtexto: {
    fontSize: 12,
    color: '#999',
    textAlign: 'center',
  },
  vacioContainer: {
    backgroundColor: '#ffffff',
    padding: 30,
    borderRadius: 10,
    alignItems: 'center',
    borderWidth: 1,
    borderColor: '#e9ecef',
    borderStyle: 'dashed',
  },
  vacioTexto: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#6c757d',
    marginBottom: 5,
  },
  vacioSubtexto: {
    fontSize: 14,
    color: '#adb5bd',
    textAlign: 'center',
  },
  infoContainer: {
    backgroundColor: '#f5f5f5',
    padding: 15,
    borderRadius: 8,
    marginBottom: 15,
    borderWidth: 1,
    borderColor: '#e0e0e0',
  },
  infoTexto: {
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 5,
  },
  infoSubtexto: {
    fontSize: 14,
    color: '#666',
  },
});

export default TareaList;

✏️ Paso 7: TareaForm.js (15 minutos)

jsx
// src/components/TareaForm.js
import React, { useState } from 'react';
import { 
  View, 
  Text, 
  TextInput, 
  TouchableOpacity, 
  StyleSheet,
  Switch,
  Alert
} from 'react-native';

function TareaForm({ onSubmit }) {
  const [formData, setFormData] = useState({
    titulo: '',
    descripcion: '',
    completada: false,
  });

  const [error, setError] = useState('');
  const [enviando, setEnviando] = useState(false);

  const handleChange = (campo, valor) => {
    setFormData({
      ...formData,
      [campo]: valor,
    });
    
    // Limpiar error si el usuario empieza a escribir
    if (error && campo === 'titulo' && valor.trim()) {
      setError('');
    }
  };

  const handleSubmit = async () => {
    // Validaci贸n
    if (!formData.titulo.trim()) {
      setError('El t铆tulo es obligatorio');
      return;
    }
    
    if (formData.titulo.length < 3) {
      setError('El t铆tulo debe tener al menos 3 caracteres');
      return;
    }
    
    setEnviando(true);
    setError('');
    
    try {
      await onSubmit(formData);
      setFormData({ titulo: '', descripcion: '', completada: false });
    } catch (error) {
      Alert.alert('❌ Error', 'No se pudo crear la tarea');
    } finally {
      setEnviando(false);
    }
  };

  return (
    <View style={styles.formulario}>
      <View style={styles.formularioContenido}>
        {error ? (
          <View style={styles.errorContainer}>
            <Text style={styles.errorTexto}>{error}</Text>
          </View>
        ) : null}
        
        {/* Campo T铆tulo */}
        <View style={styles.campo}>
          <Text style={styles.label}>T铆tulo:</Text>
          <TextInput
            style={styles.input}
            value={formData.titulo}
            onChangeText={(text) => handleChange('titulo', text)}
            placeholder="Ej: Comprar leche"
            placeholderTextColor="#999"
            editable={!enviando}
          />
        </View>
        
        {/* Campo Descripci贸n */}
        <View style={styles.campo}>
          <Text style={styles.label}>Descripci贸n:</Text>
          <TextInput
            style={[styles.input, styles.textArea]}
            value={formData.descripcion}
            onChangeText={(text) => handleChange('descripcion', text)}
            placeholder="Ej: Ir al supermercado"
            placeholderTextColor="#999"
            multiline
            numberOfLines={3}
            textAlignVertical="top"
            editable={!enviando}
          />
        </View>
        
        {/* Campo Completada */}
        <View style={styles.switchContainer}>
          <Text style={styles.switchLabel}>Marcar como completada:</Text>
          <Switch
            value={formData.completada}
            onValueChange={(value) => handleChange('completada', value)}
            disabled={enviando}
            trackColor={{ false: '#767577', true: '#81b0ff' }}
            thumbColor={formData.completada ? '#4CAF50' : '#f4f3f4'}
          />
        </View>
        
        {/* Bot贸n Crear */}
        <TouchableOpacity 
          style={[
            styles.boton,
            enviando && styles.botonDeshabilitado
          ]}
          onPress={handleSubmit}
          disabled={enviando}
        >
          {enviando ? (
            <Text style={styles.botonTexto}>⏳ Creando...</Text>
          ) : (
            <Text style={styles.botonTexto}>➕ Crear Tarea</Text>
          )}
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  formulario: {
    backgroundColor: '#ffffff',
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#dee2e6',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 2,
  },
  formularioContenido: {
    padding: 20,
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    padding: 12,
    borderRadius: 6,
    marginBottom: 15,
    borderWidth: 1,
    borderColor: '#f5c6cb',
  },
  errorTexto: {
    color: '#721c24',
    textAlign: 'center',
    fontSize: 14,
  },
  campo: {
    marginBottom: 20,
  },
  label: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
    color: '#333',
  },
  input: {
    borderWidth: 1,
    borderColor: '#ced4da',
    borderRadius: 6,
    padding: 12,
    fontSize: 16,
    color: '#333',
    backgroundColor: '#f8f9fa',
  },
  textArea: {
    height: 100,
    textAlignVertical: 'top',
  },
  switchContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 25,
    paddingHorizontal: 5,
  },
  switchLabel: {
    fontSize: 16,
    color: '#333',
    flex: 1,
  },
  boton: {
    backgroundColor: '#4CAF50',
    padding: 16,
    borderRadius: 6,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 3,
  },
  botonDeshabilitado: {
    backgroundColor: '#9E9E9E',
    opacity: 0.7,
  },
  botonTexto: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default TareaForm;

馃摝 Paso 8: TareaItem.js (15 minutos)

jsx
// src/components/TareaItem.js
import React from 'react';
import { 
  View, 
  Text, 
  TouchableOpacity, 
  StyleSheet,
  Alert 
} from 'react-native';

function TareaItem({ tarea, onEliminar, onCompletar }) {
  const confirmarEliminar = () => {
    Alert.alert(
      'Eliminar Tarea',
      '¿Est谩s seguro de eliminar esta tarea?',
      [
        { text: 'Cancelar', style: 'cancel' },
        { 
          text: 'Eliminar', 
          onPress: () => onEliminar(tarea.id), 
          style: 'destructive' 
        },
      ]
    );
  };

  return (
    <View style={[
      styles.tarjeta,
      tarea.completada && styles.tarjetaCompletada
    ]}>
      {/* Encabezado con t铆tulo */}
      <View style={styles.encabezado}>
        <Text style={[
          styles.titulo,
          tarea.completada && styles.tituloCompletado
        ]}>
          {tarea.completada ? '✅ ' : '⏳ '}
          {tarea.titulo}
        </Text>
      </View>
      
      {/* Descripci贸n */}
      {tarea.descripcion ? (
        <Text style={styles.descripcion}>
          {tarea.descripcion}
        </Text>
      ) : null}
      
      {/* Informaci贸n de la tarea */}
      <View style={styles.infoContainer}>
        <View style={styles.infoRow}>
          <Text style={styles.infoLabel}>ID:</Text>
          <Text style={styles.infoValue}>{tarea.id}</Text>
        </View>
        
        <View style={[
          styles.estadoContainer,
          tarea.completada ? styles.estadoCompletadoContainer : styles.estadoPendienteContainer
        ]}>
          <Text style={[
            styles.estadoTexto,
            tarea.completada ? styles.estadoCompletadoTexto : styles.estadoPendienteTexto
          ]}>
            {tarea.completada ? 'COMPLETADA' : 'PENDIENTE'}
          </Text>
        </View>
        
        <View style={styles.infoRow}>
          <Text style={styles.infoLabel}>馃攼</Text>
          <Text style={styles.infoValue}>Token protegido</Text>
        </View>
      </View>
      
      {/* Botones de acci贸n */}
      <View style={styles.botonesContainer}>
        <TouchableOpacity 
          style={[
            styles.botonAccion,
            tarea.completada ? styles.botonDesmarcar : styles.botonCompletar
          ]}
          onPress={() => onCompletar(tarea.id, !tarea.completada)}
        >
          <Text style={styles.botonTexto}>
            {tarea.completada ? '↩️ Desmarcar' : '✅ Completar'}
          </Text>
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={[styles.botonAccion, styles.botonEliminar]}
          onPress={confirmarEliminar}
        >
          <Text style={styles.botonTexto}>馃棏️ Eliminar</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  tarjeta: {
    backgroundColor: '#ffffff',
    padding: 20,
    borderRadius: 12,
    marginBottom: 15,
    borderWidth: 1,
    borderColor: '#e0e0e0',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
    elevation: 3,
  },
  tarjetaCompletada: {
    backgroundColor: '#f1f8e9',
    borderColor: '#c8e6c9',
  },
  encabezado: {
    marginBottom: 10,
  },
  titulo: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333',
  },
  tituloCompletado: {
    color: '#2e7d32',
  },
  descripcion: {
    fontSize: 15,
    color: '#666',
    marginBottom: 15,
    lineHeight: 22,
  },
  infoContainer: {
    backgroundColor: '#f8f9fa',
    padding: 12,
    borderRadius: 8,
    marginBottom: 15,
    borderWidth: 1,
    borderColor: '#e9ecef',
  },
  infoRow: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 6,
  },
  infoLabel: {
    fontSize: 13,
    color: '#6c757d',
    marginRight: 6,
  },
  infoValue: {
    fontSize: 13,
    color: '#495057',
    fontWeight: '500',
  },
  estadoContainer: {
    alignSelf: 'flex-start',
    paddingHorizontal: 12,
    paddingVertical: 4,
    borderRadius: 12,
    marginTop: 5,
    marginBottom: 8,
  },
  estadoCompletadoContainer: {
    backgroundColor: '#d4edda',
  },
  estadoPendienteContainer: {
    backgroundColor: '#fff3cd',
  },
  estadoTexto: {
    fontSize: 12,
    fontWeight: 'bold',
  },
  estadoCompletadoTexto: {
    color: '#155724',
  },
  estadoPendienteTexto: {
    color: '#856404',
  },
  botonesContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    gap: 12,
  },
  botonAccion: {
    flex: 1,
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 2,
    elevation: 2,
  },
  botonCompletar: {
    backgroundColor: '#4CAF50',
  },
  botonDesmarcar: {
    backgroundColor: '#FF9800',
  },
  botonEliminar: {
    backgroundColor: '#f44336',
  },
  botonTexto: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 15,
  },
});

export default TareaItem;

馃敡 Paso 9: Configurar Android/iOS para redes locales (5 minutos)

Para Android:

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<application
  ...
  android:usesCleartextTraffic="true"> <!-- Agregar esta l铆nea -->
  ...
</application>

Para iOS (Info.plist):

xml
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

O usar IP local en lugar de localhost:

javascript
// En tokenManager.js y tareaService.js
const API_URL = 'http://TU_IP_LOCAL:8000/api';
// Ejemplo: 'http://192.168.1.100:8000/api'

馃摫 Paso 10: Archivo de configuraci贸n para diferentes entornos (3 minutos)

javascript
// src/config/apiConfig.js
export const API_CONFIG = {
  development: {
    baseURL: 'http://192.168.1.100:8000/api', // Tu IP local
    timeout: 30000,
  },
  production: {
    baseURL: 'https://tuaplicacion.com/api',
    timeout: 30000,
  },
};

// Determinar entorno
const getEnvironment = () => {
  return __DEV__ ? 'development' : 'production';
};

export const currentConfig = API_CONFIG[getEnvironment()];
export const API_URL = currentConfig.baseURL;

✅ Verificaci贸n Final

Flujo de la aplicaci贸n:

  1. Primer inicio → Genera token autom谩ticamente

  2. Panel de token → Muestra token actual

  3. Formulario → Crea nuevas tareas

  4. Lista → Muestra tareas con acciones

  5. Token inv谩lido → Se regenera autom谩ticamente

  6. Manual → Bot贸n para regenerar token

Estructura visual:

text
馃攼 Gestor de Tareas con Token
API Laravel con autenticaci贸n por token

[馃攽 AUTENTICACI脫N POR TOKEN]
Token: 123456
[馃攧 Generar Nuevo Token]

[➕ NUEVA TAREA]
T铆tulo: _______________
Descripci贸n: __________
Marcar como completada: [ ]
[➕ Crear Tarea]
[馃攧 Recargar Tareas]

[馃搵 LISTA DE TAREAS]
✅ Conectado a la API
Total: 2 tareas | Token activo

• ⏳ Comprar leche
  Ir al supermercado
  ID: 1 | PENDIENTE | 馃攼 Token protegido
  [✅ Completar] [馃棏️ Eliminar]

• ✅ Estudiar React Native
  Hacer ejercicios
  ID: 2 | COMPLETADA | 馃攼 Token protegido
  [↩️ Desmarcar] [馃棏️ Eliminar]

馃搳 Resumen de Tiempo (95 minutos total)

PasoTiempoDescripci贸n
15 minConfiguraci贸n inicial con Expo
22 minEstructura de carpetas
33 minApp.js principal
410 minTokenManager con AsyncStorage
515 minServicio API con token
620 minTareaList con manejo de token
715 minTareaForm con validaci贸n
815 minTareaItem con acciones
95 minConfiguraci贸n red local
103 minConfiguraci贸n entornos

馃攽 Caracter铆sticas implementadas:

1. Sistema de Token:

  • ✅ Generaci贸n autom谩tica al primer uso

  • ✅ Almacenamiento en AsyncStorage (persistente)

  • ✅ Reintento autom谩tico si token inv谩lido

  • ✅ Panel visual del token actual

  • ✅ Bot贸n para regenerar manualmente

2. Consumo de API:

  • ✅ Headers con token en todas las peticiones

  • ✅ Manejo de errores 401 (token inv谩lido)

  • ✅ Alertas informativas para el usuario

  • ✅ Estados de carga claros

  • ✅ Reconexi贸n autom谩tica

3. Interfaz React Native:

  • ✅ Dise帽o responsive (m贸vil/tablet)

  • ✅ Componentes nativos de React Native

  • ✅ Alertas nativas para confirmaciones

  • ✅ ActivityIndicator para estados de carga

  • ✅ Estilos con StyleSheet

4. UX mejorada:

  • ✅ Validaci贸n de formularios

  • ✅ Confirmaci贸n al eliminar

  • ✅ Feedback visual inmediato

  • ✅ Manejo de errores amigable

  • ✅ Estados deshabilitados durante operaciones

馃幆 Lo que aprendi贸 el estudiante:

  1. AsyncStorage para persistencia en React Native

  2. Consumo de APIs REST con autenticaci贸n por token

  3. Manejo de headers HTTP personalizados

  4. Gesti贸n de estados de carga y error

  5. Alertas y confirmaciones nativas

  6. Dise帽o responsive con useWindowDimensions

  7. Separaci贸n de responsabilidades (servicios, componentes)

  8. Manejo de errores de red y reconexi贸n

馃挕 Tips para la explicaci贸n:

  1. Mostrar AsyncStorage usando React Native Debugger

  2. Usar la pesta帽a Network para ver las peticiones

  3. Simular error 401 modificando el token manualmente

  4. Demostrar la regeneraci贸n autom谩tica

  5. Probar en dispositivo f铆sico con IP local

  6. Mostrar el dise帽o responsive girando el dispositivo

¡Perfecto! El alumno ahora tiene una aplicaci贸n React Native completa que consume una API Laravel protegida por token, con manejo autom谩tico de autenticaci贸n y persistencia de sesi贸n, siguiendo la misma estructura que el tutorial web pero adaptada a React Native.

Comentarios

Entradas m谩s populares de este blog

0-Sistema de Tareas

13-CRUD en Laravel para Web y API con Rutas Normales (No Resource)

10-Introducci贸n a Blade en Laravel con un Ejemplo Pr谩ctico