21- Tutorial React Native Paso a Paso - Consumir API Laravel con Token
馃殌 Paso 1: Configuraci贸n Inicial (5 minutos)
# 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)
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)
// 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)
// 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)
// 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)
// 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)
// 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)
// 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:
<!-- android/app/src/main/AndroidManifest.xml -->
<application
...
android:usesCleartextTraffic="true"> <!-- Agregar esta l铆nea -->
...
</application>Para iOS (Info.plist):
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>O usar IP local en lugar de localhost:
// 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)
// 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:
Primer inicio → Genera token autom谩ticamente
Panel de token → Muestra token actual
Formulario → Crea nuevas tareas
Lista → Muestra tareas con acciones
Token inv谩lido → Se regenera autom谩ticamente
Manual → Bot贸n para regenerar token
Estructura visual:
馃攼 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)
| Paso | Tiempo | Descripci贸n |
|---|---|---|
| 1 | 5 min | Configuraci贸n inicial con Expo |
| 2 | 2 min | Estructura de carpetas |
| 3 | 3 min | App.js principal |
| 4 | 10 min | TokenManager con AsyncStorage |
| 5 | 15 min | Servicio API con token |
| 6 | 20 min | TareaList con manejo de token |
| 7 | 15 min | TareaForm con validaci贸n |
| 8 | 15 min | TareaItem con acciones |
| 9 | 5 min | Configuraci贸n red local |
| 10 | 3 min | Configuraci贸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:
AsyncStorage para persistencia en React Native
Consumo de APIs REST con autenticaci贸n por token
Manejo de headers HTTP personalizados
Gesti贸n de estados de carga y error
Alertas y confirmaciones nativas
Dise帽o responsive con useWindowDimensions
Separaci贸n de responsabilidades (servicios, componentes)
Manejo de errores de red y reconexi贸n
馃挕 Tips para la explicaci贸n:
Mostrar AsyncStorage usando React Native Debugger
Usar la pesta帽a Network para ver las peticiones
Simular error 401 modificando el token manualmente
Demostrar la regeneraci贸n autom谩tica
Probar en dispositivo f铆sico con IP local
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
Publicar un comentario