22-Tutorial React Native Sencillo - Consumir API Laravel con JWT

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

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

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

# Iniciar el proyecto
npx expo start

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

text
GestorTareasJWT/
├── src/
│   ├── components/
│   │   ├── LoginForm.js
│   │   ├── RegisterForm.js
│   │   ├── TareaList.js
│   │   ├── TareaForm.js
│   │   └── TareaItem.js
│   ├── services/
│   │   └── authService.js
│   └── App.js
├── App.js
└── package.json

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

jsx
// App.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, SafeAreaView, StatusBar } from 'react-native';
import LoginForm from './src/components/LoginForm';
import RegisterForm from './src/components/RegisterForm';
import TareaList from './src/components/TareaList';
import authService from './src/services/authService';

export default function App() {
  const [usuario, setUsuario] = useState(null);
  const [mostrarRegistro, setMostrarRegistro] = useState(false);

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

  const cargarUsuario = async () => {
    const user = await authService.getUsuario();
    setUsuario(user);
  };

  const handleLogin = (user) => {
    setUsuario(user);
  };

  const handleLogout = async () => {
    await authService.logout();
    setUsuario(null);
  };

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      
      <View style={styles.header}>
        <Text style={styles.titulo}>馃摑 Gestor de Tareas JWT</Text>
      </View>

      {usuario ? (
        <>
          <View style={styles.usuarioContainer}>
            <Text>Bienvenido, {usuario.name}</Text>
            <Text style={styles.cerrarSesion} onPress={handleLogout}>
              Cerrar Sesi贸n
            </Text>
          </View>
          <TareaList />
        </>
      ) : (
        <View style={styles.authContainer}>
          {mostrarRegistro ? (
            <RegisterForm 
              onSuccess={() => {
                setMostrarRegistro(false);
                cargarUsuario();
              }}
              onCancel={() => setMostrarRegistro(false)}
            />
          ) : (
            <LoginForm onSuccess={handleLogin} />
          )}
          
          <Text style={styles.cambioForm}>
            {mostrarRegistro ? '¿Ya tienes cuenta? ' : '¿No tienes cuenta? '}
            <Text 
              style={styles.cambioFormLink}
              onPress={() => setMostrarRegistro(!mostrarRegistro)}
            >
              {mostrarRegistro ? 'Iniciar Sesi贸n' : 'Reg铆strate'}
            </Text>
          </Text>
        </View>
      )}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  header: {
    padding: 20,
    alignItems: 'center',
  },
  titulo: {
    fontSize: 24,
    fontWeight: 'bold',
  },
  usuarioContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingHorizontal: 20,
    paddingVertical: 10,
    backgroundColor: '#e8f5e9',
  },
  cerrarSesion: {
    color: '#2196F3',
  },
  authContainer: {
    flex: 1,
    padding: 20,
  },
  cambioForm: {
    textAlign: 'center',
    marginTop: 15,
  },
  cambioFormLink: {
    color: '#2196F3',
  },
});

馃攽 Paso 4: Servicio de Autenticaci贸n (10 minutos)

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

const API_URL = 'http://192.168.1.100:8000/api'; // Cambia por tu IP

class AuthService {
  constructor() {
    this.tokenKey = 'jwt_token';
    this.userKey = 'user_data';
  }

  async setAuthData(token, user) {
    try {
      await AsyncStorage.setItem(this.tokenKey, token);
      await AsyncStorage.setItem(this.userKey, JSON.stringify(user));
    } catch (error) {
      console.error('Error al guardar datos:', error);
    }
  }

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

  async getUsuario() {
    try {
      const user = await AsyncStorage.getItem(this.userKey);
      return user ? JSON.parse(user) : null;
    } catch (error) {
      console.error('Error al obtener usuario:', error);
      return null;
    }
  }

  async isAuthenticated() {
    const token = await this.getToken();
    return !!token;
  }

  async logout() {
    try {
      await AsyncStorage.removeItem(this.tokenKey);
      await AsyncStorage.removeItem(this.userKey);
    } catch (error) {
      console.error('Error al cerrar sesi贸n:', error);
    }
  }

  getAuthHeaders() {
    return {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    };
  }

  async login(email, password) {
    try {
      console.log('Iniciando sesi贸n...');
      const response = await fetch(`${API_URL}/login`, {
        method: 'POST',
        headers: this.getAuthHeaders(),
        body: JSON.stringify({ email, password })
      });

      console.log('Respuesta login:', response.status);
      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.message || 'Error al iniciar sesi贸n');
      }

      await this.setAuthData(data.token, data.user);
      return { success: true, user: data.user };
      
    } catch (error) {
      console.error('Error login:', error);
      return { success: false, message: error.message };
    }
  }

  async register(name, email, password, password_confirmation) {
    try {
      console.log('Registrando usuario...');
      const response = await fetch(`${API_URL}/register`, {
        method: 'POST',
        headers: this.getAuthHeaders(),
        body: JSON.stringify({
          name,
          email,
          password,
          password_confirmation
        })
      });

      console.log('Respuesta registro:', response.status);
      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.message || 'Error al registrarse');
      }

      await this.setAuthData(data.token, data.user);
      return { success: true, user: data.user };
      
    } catch (error) {
      console.error('Error registro:', error);
      return { success: false, message: error.message };
    }
  }

  async logoutApi() {
    try {
      const token = await this.getToken();
      if (token) {
        await fetch(`${API_URL}/logout`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${token}`,
            'Accept': 'application/json'
          }
        });
      }
    } catch (error) {
      console.error('Error logout API:', error);
    } finally {
      await this.logout();
    }
  }
}

const authService = new AuthService();
export default authService;

馃搵 Paso 5: Servicio de Tareas (10 minutos)

jsx
// src/services/tareaService.js
import authService from './authService';

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

class TareaService {
  async checkAuth() {
    const isAuth = await authService.isAuthenticated();
    if (!isAuth) {
      throw new Error('No est谩s autenticado');
    }
  }

  async getHeaders() {
    await this.checkAuth();
    const token = await authService.getToken();
    return {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`,
      'Accept': 'application/json'
    };
  }

  async obtenerTodas() {
    try {
      const response = await fetch(`${API_URL}/tareas`, {
        method: 'GET',
        headers: await this.getHeaders()
      });

      if (response.status === 401) {
        await authService.logout();
        throw new Error('Sesi贸n expirada');
      }

      if (!response.ok) {
        throw new Error('Error al obtener tareas');
      }

      const data = await response.json();
      return data;
      
    } catch (error) {
      console.error('Error obtenerTodas:', error);
      throw error;
    }
  }

  async crearTarea(tareaData) {
    try {
      const response = await fetch(`${API_URL}/tareas`, {
        method: 'POST',
        headers: await this.getHeaders(),
        body: JSON.stringify(tareaData)
      });

      if (response.status === 401) {
        await authService.logout();
        throw new Error('Sesi贸n expirada');
      }

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message || 'Error al crear tarea');
      }

      return await response.json();
      
    } catch (error) {
      console.error('Error crearTarea:', error);
      throw error;
    }
  }

  async actualizarTarea(id, tareaData) {
    try {
      const response = await fetch(`${API_URL}/tareas/${id}`, {
        method: 'PUT',
        headers: await this.getHeaders(),
        body: JSON.stringify(tareaData)
      });

      if (response.status === 401) {
        await authService.logout();
        throw new Error('Sesi贸n expirada');
      }

      if (!response.ok) {
        throw new Error('Error al actualizar tarea');
      }

      return await response.json();
      
    } catch (error) {
      console.error('Error actualizarTarea:', error);
      throw error;
    }
  }

  async eliminarTarea(id) {
    try {
      const response = await fetch(`${API_URL}/tareas/${id}`, {
        method: 'DELETE',
        headers: await this.getHeaders()
      });

      if (response.status === 401) {
        await authService.logout();
        throw new Error('Sesi贸n expirada');
      }

      if (!response.ok) {
        throw new Error('Error al eliminar tarea');
      }

      return await response.json();
      
    } catch (error) {
      console.error('Error eliminarTarea:', error);
      throw error;
    }
  }
}

const tareaService = new TareaService();
export default tareaService;

✏️ Paso 6: LoginForm.js (10 minutos)

jsx
// src/components/LoginForm.js
import React, { useState } from 'react';
import { 
  View, 
  Text, 
  TextInput, 
  TouchableOpacity, 
  StyleSheet,
  ActivityIndicator 
} from 'react-native';
import authService from '../services/authService';

function LoginForm({ onSuccess }) {
  const [formData, setFormData] = useState({
    email: '',
    password: ''
  });
  const [error, setError] = useState('');
  const [cargando, setCargando] = useState(false);

  const handleChange = (campo, valor) => {
    setFormData({
      ...formData,
      [campo]: valor
    });
  };

  const handleSubmit = async () => {
    setError('');
    setCargando(true);

    try {
      const resultado = await authService.login(formData.email, formData.password);
      
      if (resultado.success) {
        onSuccess(resultado.user);
      } else {
        setError(resultado.message || 'Error al iniciar sesi贸n');
      }
    } catch (error) {
      setError('Error de conexi贸n');
    } finally {
      setCargando(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.titulo}>Iniciar Sesi贸n</Text>
      
      {error ? (
        <View style={styles.errorContainer}>
          <Text style={styles.errorTexto}>{error}</Text>
        </View>
      ) : null}
      
      <View style={styles.campo}>
        <Text style={styles.label}>Email:</Text>
        <TextInput
          style={styles.input}
          value={formData.email}
          onChangeText={(text) => handleChange('email', text)}
          placeholder="tu@email.com"
          keyboardType="email-address"
          editable={!cargando}
        />
      </View>
      
      <View style={styles.campo}>
        <Text style={styles.label}>Contrase帽a:</Text>
        <TextInput
          style={styles.input}
          value={formData.password}
          onChangeText={(text) => handleChange('password', text)}
          placeholder="Tu contrase帽a"
          secureTextEntry
          editable={!cargando}
        />
      </View>
      
      <TouchableOpacity 
        style={[styles.boton, cargando && styles.botonDeshabilitado]}
        onPress={handleSubmit}
        disabled={cargando}
      >
        {cargando ? (
          <ActivityIndicator color="white" />
        ) : (
          <Text style={styles.botonTexto}>Iniciar Sesi贸n</Text>
        )}
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#ccc',
  },
  titulo: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 15,
    textAlign: 'center',
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    padding: 10,
    borderRadius: 5,
    marginBottom: 15,
  },
  errorTexto: {
    color: '#c62828',
    textAlign: 'center',
  },
  campo: {
    marginBottom: 15,
  },
  label: {
    marginBottom: 5,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 5,
    padding: 10,
    fontSize: 16,
  },
  boton: {
    backgroundColor: '#4CAF50',
    padding: 15,
    borderRadius: 5,
    alignItems: 'center',
  },
  botonDeshabilitado: {
    opacity: 0.6,
  },
  botonTexto: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default LoginForm;

馃摑 Paso 7: RegisterForm.js (10 minutos)

jsx
// src/components/RegisterForm.js
import React, { useState } from 'react';
import { 
  View, 
  Text, 
  TextInput, 
  TouchableOpacity, 
  StyleSheet,
  ActivityIndicator 
} from 'react-native';
import authService from '../services/authService';

function RegisterForm({ onSuccess, onCancel }) {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    password_confirmation: ''
  });
  const [error, setError] = useState('');
  const [cargando, setCargando] = useState(false);

  const handleChange = (campo, valor) => {
    setFormData({
      ...formData,
      [campo]: valor
    });
  };

  const handleSubmit = async () => {
    if (formData.password !== formData.password_confirmation) {
      setError('Las contrase帽as no coinciden');
      return;
    }
    
    if (formData.password.length < 6) {
      setError('La contrase帽a debe tener al menos 6 caracteres');
      return;
    }
    
    setError('');
    setCargando(true);

    try {
      const resultado = await authService.register(
        formData.name,
        formData.email,
        formData.password,
        formData.password_confirmation
      );
      
      if (resultado.success) {
        onSuccess(resultado.user);
      } else {
        setError(resultado.message || 'Error al registrarse');
      }
    } catch (error) {
      setError('Error de conexi贸n');
    } finally {
      setCargando(false);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.titulo}>Registrarse</Text>
      
      {error ? (
        <View style={styles.errorContainer}>
          <Text style={styles.errorTexto}>{error}</Text>
        </View>
      ) : null}
      
      <View style={styles.campo}>
        <Text style={styles.label}>Nombre:</Text>
        <TextInput
          style={styles.input}
          value={formData.name}
          onChangeText={(text) => handleChange('name', text)}
          placeholder="Tu nombre"
          editable={!cargando}
        />
      </View>
      
      <View style={styles.campo}>
        <Text style={styles.label}>Email:</Text>
        <TextInput
          style={styles.input}
          value={formData.email}
          onChangeText={(text) => handleChange('email', text)}
          placeholder="tu@email.com"
          keyboardType="email-address"
          editable={!cargando}
        />
      </View>
      
      <View style={styles.campo}>
        <Text style={styles.label}>Contrase帽a:</Text>
        <TextInput
          style={styles.input}
          value={formData.password}
          onChangeText={(text) => handleChange('password', text)}
          placeholder="M铆nimo 6 caracteres"
          secureTextEntry
          editable={!cargando}
        />
      </View>
      
      <View style={styles.campo}>
        <Text style={styles.label}>Confirmar Contrase帽a:</Text>
        <TextInput
          style={styles.input}
          value={formData.password_confirmation}
          onChangeText={(text) => handleChange('password_confirmation', text)}
          placeholder="Repite tu contrase帽a"
          secureTextEntry
          editable={!cargando}
        />
      </View>
      
      <View style={styles.botonesContainer}>
        <TouchableOpacity 
          style={[styles.boton, styles.botonRegistro, cargando && styles.botonDeshabilitado]}
          onPress={handleSubmit}
          disabled={cargando}
        >
          {cargando ? (
            <ActivityIndicator color="white" />
          ) : (
            <Text style={styles.botonTexto}>Registrarse</Text>
          )}
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={[styles.boton, styles.botonCancelar]}
          onPress={onCancel}
          disabled={cargando}
        >
          <Text style={styles.botonTexto}>Cancelar</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: '#ccc',
  },
  titulo: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 15,
    textAlign: 'center',
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    padding: 10,
    borderRadius: 5,
    marginBottom: 15,
  },
  errorTexto: {
    color: '#c62828',
    textAlign: 'center',
  },
  campo: {
    marginBottom: 15,
  },
  label: {
    marginBottom: 5,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 5,
    padding: 10,
    fontSize: 16,
  },
  botonesContainer: {
    flexDirection: 'row',
    gap: 10,
  },
  boton: {
    flex: 1,
    padding: 15,
    borderRadius: 5,
    alignItems: 'center',
  },
  botonRegistro: {
    backgroundColor: '#2196F3',
  },
  botonCancelar: {
    backgroundColor: '#9E9E9E',
  },
  botonDeshabilitado: {
    opacity: 0.6,
  },
  botonTexto: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default RegisterForm;

馃搵 Paso 8: TareaList.js (15 minutos)

jsx
// src/components/TareaList.js
import React, { useState, useEffect } from 'react';
import { 
  View, 
  Text, 
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  ActivityIndicator 
} 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('');

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

  const cargarTareas = async () => {
    try {
      setCargando(true);
      setError('');
      const datos = await tareaService.obtenerTodas();
      setTareas(datos);
    } catch (error) {
      if (error.message.includes('Sesi贸n expirada')) {
        setError('Tu sesi贸n ha expirado');
      } else {
        setError('Error al cargar tareas');
      }
    } finally {
      setCargando(false);
    }
  };

  const manejarCrearTarea = async (nuevaTarea) => {
    try {
      await tareaService.crearTarea(nuevaTarea);
      await cargarTareas();
    } catch (error) {
      alert('Error al crear la tarea');
    }
  };

  const manejarEliminarTarea = async (id) => {
    try {
      await tareaService.eliminarTarea(id);
      await cargarTareas();
    } catch (error) {
      alert('Error al eliminar la tarea');
    }
  };

  const manejarCompletarTarea = async (id, completada) => {
    try {
      await tareaService.actualizarTarea(id, { completada });
      await cargarTareas();
    } catch (error) {
      alert('Error al actualizar la tarea');
    }
  };

  return (
    <ScrollView style={styles.container}>
      {/* Formulario */}
      <View style={styles.seccion}>
        <Text style={styles.tituloSeccion}>➕ Nueva Tarea</Text>
        <TareaForm onSubmit={manejarCrearTarea} />
      </View>

      {/* Lista */}
      <View style={styles.seccion}>
        <Text style={styles.tituloSeccion}>馃搵 Mis Tareas</Text>
        
        {error ? (
          <View style={styles.errorContainer}>
            <Text style={styles.errorTexto}>{error}</Text>
          </View>
        ) : null}
        
        {cargando ? (
          <View style={styles.cargandoContainer}>
            <ActivityIndicator size="large" />
            <Text>Cargando tus tareas...</Text>
          </View>
        ) : tareas.length === 0 ? (
          <Text style={styles.vacio}>No tienes tareas. ¡Crea tu primera tarea!</Text>
        ) : (
          <>
            <Text style={styles.contador}>Total: {tareas.length} tareas</Text>
            
            {tareas.map((tarea) => (
              <TareaItem
                key={tarea.id}
                tarea={tarea}
                onEliminar={manejarEliminarTarea}
                onCompletar={manejarCompletarTarea}
              />
            ))}
          </>
        )}
        
        <TouchableOpacity 
          style={styles.botonRecargar}
          onPress={cargarTareas}
        >
          <Text style={styles.botonRecargarTexto}>馃攧 Recargar Tareas</Text>
        </TouchableOpacity>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 15,
  },
  seccion: {
    marginBottom: 20,
  },
  tituloSeccion: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    padding: 15,
    borderRadius: 5,
    marginBottom: 15,
  },
  errorTexto: {
    color: '#c62828',
    textAlign: 'center',
  },
  cargandoContainer: {
    alignItems: 'center',
    padding: 30,
  },
  vacio: {
    textAlign: 'center',
    padding: 20,
    color: '#666',
  },
  contador: {
    marginBottom: 10,
  },
  botonRecargar: {
    backgroundColor: '#4CAF50',
    padding: 12,
    borderRadius: 5,
    alignItems: 'center',
    marginTop: 10,
  },
  botonRecargarTexto: {
    color: 'white',
    fontWeight: 'bold',
  },
});

export default TareaList;

✏️ Paso 9: TareaForm.js (10 minutos)

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

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

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

  const handleChange = (campo, valor) => {
    setFormData({
      ...formData,
      [campo]: valor
    });
  };

  const handleSubmit = () => {
    if (!formData.nombre.trim()) {
      setError('El nombre es obligatorio');
      return;
    }
    
    onSubmit(formData);
    setFormData({ nombre: '', descripcion: '', completada: false });
    setError('');
  };

  return (
    <View style={styles.container}>
      {error ? (
        <View style={styles.errorContainer}>
          <Text style={styles.errorTexto}>{error}</Text>
        </View>
      ) : null}
      
      <View style={styles.campo}>
        <Text style={styles.label}>Nombre de la tarea:</Text>
        <TextInput
          style={styles.input}
          value={formData.nombre}
          onChangeText={(text) => handleChange('nombre', text)}
          placeholder="Ej: Comprar leche"
        />
      </View>
      
      <View style={styles.campo}>
        <Text style={styles.label}>Descripci贸n (opcional):</Text>
        <TextInput
          style={[styles.input, styles.textArea]}
          value={formData.descripcion}
          onChangeText={(text) => handleChange('descripcion', text)}
          placeholder="Ej: Ir al supermercado"
          multiline
          numberOfLines={3}
        />
      </View>
      
      <View style={styles.switchContainer}>
        <Text>Marcar como completada:</Text>
        <Switch
          value={formData.completada}
          onValueChange={(value) => handleChange('completada', value)}
        />
      </View>
      
      <TouchableOpacity style={styles.boton} onPress={handleSubmit}>
        <Text style={styles.botonTexto}>➕ Crear Tarea</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#f8f9fa',
    padding: 15,
    borderRadius: 5,
    borderWidth: 1,
    borderColor: '#dee2e6',
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    padding: 10,
    borderRadius: 5,
    marginBottom: 15,
  },
  errorTexto: {
    color: '#c62828',
    textAlign: 'center',
  },
  campo: {
    marginBottom: 15,
  },
  label: {
    marginBottom: 5,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ced4da',
    borderRadius: 5,
    padding: 10,
    fontSize: 16,
  },
  textArea: {
    height: 80,
  },
  switchContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 15,
  },
  boton: {
    backgroundColor: '#4CAF50',
    padding: 15,
    borderRadius: 5,
    alignItems: 'center',
  },
  botonTexto: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

export default TareaForm;

馃摝 Paso 10: TareaItem.js (10 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) },
      ]
    );
  };

  return (
    <View style={[
      styles.container,
      tarea.completada && styles.containerCompletada
    ]}>
      <View style={styles.header}>
        <Text style={[
          styles.titulo,
          tarea.completada && styles.tituloCompletado
        ]}>
          {tarea.completada ? '✅ ' : '⏳ '}
          {tarea.nombre}
        </Text>
      </View>
      
      {tarea.descripcion ? (
        <Text style={styles.descripcion}>{tarea.descripcion}</Text>
      ) : null}
      
      <View style={styles.info}>
        <Text>ID: {tarea.id}</Text>
        <Text>Estado: {tarea.completada ? 'Completada' : 'Pendiente'}</Text>
        {tarea.created_at && (
          <Text>Creada: {new Date(tarea.created_at).toLocaleDateString()}</Text>
        )}
      </View>
      
      <View style={styles.botonesContainer}>
        <TouchableOpacity 
          style={[
            styles.boton,
            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.boton, styles.botonEliminar]}
          onPress={confirmarEliminar}
        >
          <Text style={styles.botonTexto}>馃棏️ Eliminar</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 5,
    marginBottom: 10,
    borderWidth: 1,
    borderColor: '#dee2e6',
  },
  containerCompletada: {
    backgroundColor: '#e8f5e9',
  },
  header: {
    marginBottom: 8,
  },
  titulo: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  tituloCompletado: {
    color: '#2e7d32',
  },
  descripcion: {
    color: '#666',
    marginBottom: 10,
  },
  info: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 15,
  },
  botonesContainer: {
    flexDirection: 'row',
    gap: 10,
  },
  boton: {
    flex: 1,
    padding: 10,
    borderRadius: 5,
    alignItems: 'center',
  },
  botonCompletar: {
    backgroundColor: '#4CAF50',
  },
  botonDesmarcar: {
    backgroundColor: '#FF9800',
  },
  botonEliminar: {
    backgroundColor: '#f44336',
  },
  botonTexto: {
    color: 'white',
    fontWeight: 'bold',
  },
});

export default TareaItem;

馃敡 Paso 11: Configuraci贸n para Android/iOS (2 minutos)

Para Android (AndroidManifest.xml):

xml
<application
  android:usesCleartextTraffic="true">

Cambiar la IP en los servicios:

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

✅ Verificaci贸n Final

Flujo de la aplicaci贸n:

text
馃摑 Gestor de Tareas JWT

[NO AUTENTICADO]
Iniciar Sesi贸n
Email: _______________
Contrase帽a: __________
[Iniciar Sesi贸n]
¿No tienes cuenta? Reg铆strate

Registrarse
Nombre: _______________
Email: _______________
Contrase帽a: __________
Confirmar Contrase帽a: ___
[Registrarse] [Cancelar]

[AUTENTICADO]
Bienvenido, [Nombre] | Cerrar Sesi贸n

➕ Nueva Tarea
Nombre: _______________
Descripci贸n: __________
Marcar como completada: [ ]
[➕ Crear Tarea]

馃搵 Mis Tareas
Total: X tareas

• ⏳ Comprar leche
  Ir al supermercado
  ID: 1 | Estado: Pendiente | Creada: 2024-01-17
  [✅ Completar] [馃棏️ Eliminar]

• ✅ Estudiar React Native
  Hacer ejercicios
  ID: 2 | Estado: Completada | Creada: 2024-01-16
  [↩️ Desmarcar] [馃棏️ Eliminar]

[馃攧 Recargar Tareas]

馃搳 Resumen de Tiempo (90 minutos)

PasoTiempoDescripci贸n
15 minConfiguraci贸n inicial
22 minEstructura de carpetas
33 minApp.js principal
410 minServicio de autenticaci贸n
510 minServicio de tareas
610 minLoginForm
710 minRegisterForm
815 minTareaList
910 minTareaForm
1010 minTareaItem
112 minConfiguraci贸n Android/iOS

✅ Caracter铆sticas implementadas:

1. Autenticaci贸n JWT:

  • ✅ Registro de usuarios

  • ✅ Inicio de sesi贸n

  • ✅ Persistencia con AsyncStorage

  • ✅ Cierre de sesi贸n

  • ✅ Headers con token Bearer

2. CRUD de tareas:

  • ✅ Listar tareas del usuario

  • ✅ Crear nuevas tareas

  • ✅ Marcar como completadas

  • ✅ Eliminar tareas

3. Interfaz sencilla:

  • ✅ Estilos m铆nimos con StyleSheet

  • ✅ Formularios b谩sicos

  • ✅ Alertas nativas

  • ✅ Estados de carga

4. Seguridad:

  • ✅ Token JWT en todas las peticiones

  • ✅ Verificaci贸n de autenticaci贸n

  • ✅ Cada usuario ve solo sus tareas

  • ✅ Manejo de sesiones expiradas

馃幆 Lo que aprendi贸 el estudiante:

  1. AsyncStorage para persistencia en React Native

  2. Consumo de APIs con autenticaci贸n JWT

  3. Componentes b谩sicos de React Native

  4. Manejo de formularios en m贸vil

  5. Navegaci贸n simple entre pantallas

  6. Alertas nativas para confirmaciones

  7. Estados de carga y error

馃挕 Tips para probar:

  1. Configurar Laravel con Sanctum primero

  2. Usar IP local en lugar de localhost

  3. Probar con Postman las rutas de la API

  4. Verificar AsyncStorage con React Native Debugger

  5. Probar en dispositivo f铆sico para mejor experiencia

¡Perfecto! El alumno ahora tiene una aplicaci贸n React Native sencilla pero completa que consume una API Laravel con autenticaci贸n JWT, usando solo los componentes y estilos necesarios para funcionar correctamente.

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