22-Tutorial React Native Sencillo - Consumir API Laravel con JWT
馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)
# 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)
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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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):
<application
android:usesCleartextTraffic="true">Cambiar la IP en los servicios:
// 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:
馃摑 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)
| Paso | Tiempo | Descripci贸n |
|---|---|---|
| 1 | 5 min | Configuraci贸n inicial |
| 2 | 2 min | Estructura de carpetas |
| 3 | 3 min | App.js principal |
| 4 | 10 min | Servicio de autenticaci贸n |
| 5 | 10 min | Servicio de tareas |
| 6 | 10 min | LoginForm |
| 7 | 10 min | RegisterForm |
| 8 | 15 min | TareaList |
| 9 | 10 min | TareaForm |
| 10 | 10 min | TareaItem |
| 11 | 2 min | Configuraci贸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:
AsyncStorage para persistencia en React Native
Consumo de APIs con autenticaci贸n JWT
Componentes b谩sicos de React Native
Manejo de formularios en m贸vil
Navegaci贸n simple entre pantallas
Alertas nativas para confirmaciones
Estados de carga y error
馃挕 Tips para probar:
Configurar Laravel con Sanctum primero
Usar IP local en lugar de localhost
Probar con Postman las rutas de la API
Verificar AsyncStorage con React Native Debugger
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
Publicar un comentario