Integrar autenticación y pasarela de pagos simulada

Lectura
25 min~12 min lectura

Introducción: La Columna Vertebral de la Experiencia de Usuario

En el desarrollo de una aplicación de comercio electrónico, dos componentes se erigen como pilares fundamentales de la confianza y la funcionalidad: la autenticación de usuarios y el procesamiento de pagos. La primera es la puerta de entrada personalizada que permite a los usuarios tener un carrito persistente, un historial de pedidos y una experiencia única. La segunda es el momento culminante de la transacción, donde la confianza del usuario debe ser absoluta. Integrar estos sistemas de forma segura, fluida y mantenible es lo que separa una aplicación de juguete de una lista para producción.

En esta lección, nos enfocaremos en construir un flujo completo que abarca desde el registro y login del usuario hasta la simulación de una transacción financiera. Utilizaremos Firebase Authentication por su robustez, integración sencilla con Expo y su suite gratuita inicial, perfecta para prototipos y lanzamientos. Para los pagos, implementaremos una pasarela de pagos simulada que replicará fielmente la arquitectura, flujo de datos y manejo de estados de un sistema real (como Stripe o Mercado Pago), pero sin realizar cargos reales, lo cual es ideal para desarrollo y testing.

Este enfoque nos permite aprender todos los conceptos críticos: manejo de sesiones, tokens de acceso, seguridad de rutas, envío de datos sensibles a un backend y gestión de estados complejos (éxito, pendiente, fallo) en la UI. Al final, tendrás un módulo funcional que podrás conectar a una pasarela real simplemente cambiando el endpoint de la API.

Concepto Clave: Tokens, Sesiones y el Flujo de Confianza

Imagina que la autenticación es como entrar a un club exclusivo. Primero, te registras mostrando tu ID (email) y recibes una ficha de membresía única (contraseña hasheada). Cuando quieres entrar, muestras tu ID y das una contraseña. El portero (Firebase) verifica contra su lista, y si es correcto, te da un pase de un día (Token JWT) especial, con tu nombre y nivel de acceso, pero con tinta invisible que solo el club puede validar. Este pase es tu sesión. En React Native, guardamos este pase de forma segura (en el Keychain del dispositivo usando AsyncStorage o SecureStore de Expo) y lo mostramos cada vez que queremos acceder a un área privada del club (como tu perfil o tu carrito).

La pasarela de pagos simulada funciona como una transacción de práctica en un banco. Realizas todos los movimientos: llenas el formulario con los datos de la tarjeta, firmas, recibes un comprobante y ves cómo se actualiza tu saldo "de prueba". Todos los protocolos de comunicación (HTTPS, envío de datos estructurados, respuestas del servidor) son idénticos a los reales, pero el dinero es ficticio. Esto nos permite desarrollar y probar exhaustivamente el flujo completo de checkout, manejo de errores de tarjeta declinada y confirmación de pedido, sin riesgo financiero. La clave está en estructurar el código de manera que, al pasar a producción, solo debas reemplazar la URL de la API simulada por la real y ajustar los formatos de datos específicos.

Cómo Funciona en la Práctica: Arquitectura del Flujo Integrado

El flujo comienza con el usuario. Implementaremos un stack de navegación que contenga rutas públicas (Login, Registro, Catálogo) y rutas privadas (Perfil, Carrito, Checkout). Un componente de alto orden o un contexto (AuthContext) se encargará de consultar si existe un token de sesión válido almacenado y, en función de ello, dirigir al usuario al stack correspondiente. Cuando un usuario inicia sesión exitosamente, Firebase devuelve un objeto de usuario con un ID token. Este token debe ser almacenado de forma segura y también enviado (en el encabezado Authorization) a nuestro backend personalizado o a Cloud Functions para cualquier operación sensible.

Para el pago, el proceso es secuencial. Primero, el usuario navega a su carrito y procede al checkout. En esta pantalla, recopilamos la información de envío y mostramos un formulario para los datos de pago simulados (número de tarjeta, fecha, CVV). Al presionar "Pagar", la aplicación no envía estos datos directamente a Firebase. En su lugar, los envía mediante una petición POST a nuestro backend simulado (que podemos construir con Express.js y desplegar en servicios como Render o Railway). Este backend valida la estructura de los datos, simula una comunicación con una entidad bancaria, genera un ID de transacción ficticio y responde con un estado (éxito, fallo). Nuestra app recibe esta respuesta y actualiza la UI en consecuencia, navegando a una pantalla de confirmación o mostrando un error.

Tip Crítico: Nunca, bajo ninguna circunstancia, debes almacenar o registrar en consola los datos reales de tarjetas de crédito en el lado del cliente (React Native). La pasarela simulada debe ser un backend separado que, en un escenario real, se reemplazaría por un servicio certificado PCI DSS. Para datos sensibles reales, usa SDKs oficiales que empleen elementos de UI nativos o redirijan al navegador.

Código en Acción: Implementando AuthContext y el Checkout Simulado

A continuación, crearemos el corazón de nuestra autenticación: un contexto de React que maneje el estado global del usuario, las funciones de login/registro y la persistencia de la sesión. Luego, construiremos la pantalla de checkout que consume nuestra API de pagos simulada.

1. Contexto de Autenticación con Firebase y Persistencia


// contexts/AuthContext.js
import React, { createContext, useState, useEffect, useContext } from 'react';
import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut } from 'firebase/auth';
import { auth } from '../config/firebase'; // Configuración inicial de Firebase
import * as SecureStore from 'expo-secure-store';

const AuthContext = createContext({});

export const useAuth = () => useContext(AuthContext);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Intentar cargar el token almacenado al iniciar la app
    const loadStoredUser = async () => {
      try {
        const userJSON = await SecureStore.getItemAsync('user_session');
        if (userJSON) {
          setUser(JSON.parse(userJSON));
        }
      } catch (error) {
        console.error('Error cargando sesión:', error);
      } finally {
        setLoading(false);
      }
    };

    // También nos suscribimos al observador de estado de Firebase
    const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
      if (firebaseUser) {
        // Obtener el token JWT
        const token = await firebaseUser.getIdToken();
        const userData = {
          uid: firebaseUser.uid,
          email: firebaseUser.email,
          token: token,
        };
        setUser(userData);
        // Guardar de forma segura
        await SecureStore.setItemAsync('user_session', JSON.stringify(userData));
      } else {
        setUser(null);
        await SecureStore.deleteItemAsync('user_session');
      }
      setLoading(false);
    });

    loadStoredUser();
    return unsubscribe; // Limpiar suscripción al desmontar
  }, []);

  const login = async (email, password) => {
    try {
      const userCredential = await signInWithEmailAndPassword(auth, email, password);
      // El efecto `onAuthStateChanged` se disparará y manejará el estado.
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  };

  const register = async (email, password) => {
    try {
      await createUserWithEmailAndPassword(auth, email, password);
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  };

  const logout = async () => {
    try {
      await signOut(auth);
      // El efecto `onAuthStateChanged` se disparará y limpiará el estado.
    } catch (error) {
      console.error('Error al cerrar sesión:', error);
    }
  };

  const value = {
    user,
    loading,
    login,
    register,
    logout,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
    

2. Pantalla de Checkout con Petición a Pasarela Simulada


// screens/CheckoutScreen.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button, Alert, ScrollView, ActivityIndicator } from 'react-native';
import { useAuth } from '../contexts/AuthContext';
import { API_URL } from '@env'; // URL de tu backend simulado

const CheckoutScreen = ({ navigation, route }) => {
  const { cartTotal } = route.params; // Total pasado desde el carrito
  const { user } = useAuth();
  const [loading, setLoading] = useState(false);
  const [form, setForm] = useState({
    cardNumber: '4242424242424242', // Número de prueba estándar
    expiryMonth: '12',
    expiryYear: '34',
    cvc: '123',
    cardholderName: `${user?.email}`,
  });

  const handlePayment = async () => {
    if (!user || !user.token) {
      Alert.alert('Error', 'Debes iniciar sesión para pagar.');
      return;
    }

    setLoading(true);
    const paymentPayload = {
      amount: cartTotal * 100, // Convertir a centavos/céntimos
      currency: 'usd',
      source: {
        number: form.cardNumber.replace(/\s/g, ''),
        exp_month: parseInt(form.expiryMonth),
        exp_year: parseInt(form.expiryYear),
        cvc: form.cvc,
        name: form.cardholderName,
      },
      userId: user.uid,
      description: `Compra desde la app - ${user.email}`,
    };

    try {
      const response = await fetch(`${API_URL}/api/payments/process`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${user.token}`, // Enviamos el token de Firebase
        },
        body: JSON.stringify(paymentPayload),
      });

      const data = await response.json();

      if (response.ok && data.status === 'succeeded') {
        // Navegar a pantalla de éxito con el ID de transacción
        navigation.replace('PaymentSuccess', { transactionId: data.id });
      } else {
        Alert.alert('Pago Fallido', data.message || 'No se pudo procesar el pago.');
      }
    } catch (error) {
      console.error('Error en la petición de pago:', error);
      Alert.alert('Error de Conexión', 'No se pudo contactar con el servidor.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <ScrollView contentContainerStyle={{ padding: 20 }}>
      <Text style={{fontSize: 20, fontWeight: 'bold', marginBottom: 20}}>Checkout</Text>
      <Text>Total a pagar: ${cartTotal.toFixed(2)}</Text>

      <Text style={{marginTop: 15}}>Número de Tarjeta (Simulada)</Text>
      <TextInput
        value={form.cardNumber}
        onChangeText={(text) => setForm({...form, cardNumber: text})}
        placeholder="4242 4242 4242 4242"
        keyboardType="numeric"
        style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
      />

      <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
        <View style={{ flex: 1, marginRight: 5 }}>
          <Text>Mes Exp.</Text>
          <TextInput value={form.expiryMonth} onChangeText={(t) => setForm({...form, expiryMonth: t})} style={{ borderWidth: 1, padding: 10 }} />
        </View>
        <View style={{ flex: 1, marginLeft: 5 }}>
          <Text>Año Exp.</Text>
          <TextInput value={form.expiryYear} onChangeText={(t) => setForm({...form, expiryYear: t})} style={{ borderWidth: 1, padding: 10 }} />
        </View>
        <View style={{ flex: 1, marginLeft: 10 }}>
          <Text>CVC</Text>
          <TextInput value={form.cvc} onChangeText={(t) => setForm({...form, cvc: t})} style={{ borderWidth: 1, padding: 10 }} />
        </View>
      </View>

      {loading ? (
        <ActivityIndicator size="large" style={{ marginTop: 30 }} />
      ) : (
        <Button title={`Pagar $${cartTotal.toFixed(2)}`} onPress={handlePayment} />
      )}
    </ScrollView>
  );
};

export default CheckoutScreen;
    

3. Backend Simulado (Ejemplo en Node.js/Express)


// server/index.js (Backend Simulado)
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3001;

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

// Middleware para verificar token de Firebase (simplificado)
const verifyToken = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token no proporcionado' });
  }
  const token = authHeader.split(' ')[1];
  // Aquí, en un entorno real, verificarías el token con el SDK Admin de Firebase.
  // Para simulación, asumimos que es válido si tiene formato.
  console.log('Token recibido (simulado):', token.substring(0, 20) + '...');
  next();
};

app.post('/api/payments/process', verifyToken, (req, res) => {
  const { amount, source, userId } = req.body;
  console.log(`Simulando pago de ${amount} centavos para usuario ${userId}`);

  // Lógica de simulación: "declinar" si el número termina en '0000'
  if (source.number.endsWith('0000')) {
    return res.status(400).json({
      status: 'failed',
      id: `sim_${Date.now()}`,
      message: 'Tarjeta declinada. Por favor, usa otra forma de pago.',
    });
  }

  // Simular un retraso de red y procesamiento
  setTimeout(() => {
    res.status(200).json({
      status: 'succeeded',
      id: `sim_${Date.now()}`, // ID de transacción simulado
      amount: amount,
      currency: 'usd',
      description: 'Pago simulado exitoso',
    });
  }, 1500);
});

app.listen(PORT, () => {
  console.log(`Servidor de pagos simulado corriendo en puerto ${PORT}`);
});
    

Errores Comunes y Cómo Evitarlos

1. Almacenar el Token de Firebase en AsyncStorage sin cifrar: AsyncStorage no es seguro para datos sensibles. Un dispositivo con root/jailbreak podría acceder a él. Solución: Usa siempre expo-secure-store o react-native-keychain para almacenar tokens y credenciales. Estas APIs utilizan el Keychain (iOS) y Keystore (Android) del sistema operativo.

2. No manejar el estado de "carga" (loading) durante la autenticación: Esto provoca que el usuario vea pantallas de login brevemente aunque ya tenga una sesión válida, o que pueda interactuar con la UI antes de que se verifique su identidad. Solución: Implementa un estado loading booleano en tu AuthContext (como se muestra en el código) y muestra un componente de carga (un ActivityIndicator o Splash Screen) mientras se resuelve la sesión persistente.

3. Enviar datos de pago directamente desde la app a Firebase o a un servicio externo sin un backend propio: Esto expone las claves secretas de tu pasarela de pagos en el código del cliente, lo cual es una gravísima vulnerabilidad de seguridad. Solución: Siempre interpone un backend propio (serverless function o servidor dedicado) entre tu app y la pasarela de pagos. Este backend es el que almacena las claves secretas y realiza la comunicación segura.

4. No validar los campos del formulario de pago en el frontend y backend: Enviar datos mal formateados (como un mes '13' o un CVC con letras) genera errores innecesarios y mala experiencia de usuario. Solución: Implementa validación en tiempo real en la UI (usando librerías como Formik + Yup) y siempre replica esa validación en el backend. El backend nunca debe confiar plenamente en el frontend.

5. Olvidar limpiar el carrito y el estado de la aplicación después de un pago exitoso: Si el usuario vuelve atrás después de pagar, podría encontrar su carrito aún lleno e intentar pagar nuevamente. Solución: Tras una confirmación de pago exitosa, debes despachar acciones para vaciar el carrito (en tu estado global, e.g., Redux o Context) y posiblemente navegar con navigation.replace para que no se pueda volver a la pantalla de checkout con el botón "atrás".

Checklist de Dominio

Antes de considerar esta lección completa, asegúrate de poder verificar cada uno de los siguientes puntos:

  • Puedo explicar la diferencia entre autenticación (quién eres) y autorización (qué puedes hacer), y cómo un token JWT sirve para ambos propósitos en nuestro flujo.
  • He implementado un AuthContext funcional que persiste la sesión del usuario usando SecureStore y reacciona a cambios en el estado de Firebase Auth.
  • Mi aplicación tiene una navegación segregada: rutas públicas (Login, Registro) y un stack privado (Home, Perfil, Carrito) al que solo se accede con sesión válida.
  • Puedo realizar una petición POST a un backend simulado, enviando el token de autenticación en los headers y manejando correctamente las respuestas de éxito y error.
  • He simulado al menos tres escenarios de pago: exitoso, tarjeta declinada y error de conexión, manejando cada uno con feedback claro al usuario en la UI.
  • Comprendo por qué es crítico tener un backend intermedio para procesar pagos reales y no hacerlo directamente desde la app móvil.
  • Puedo listar al menos tres buenas prácticas de seguridad para el manejo de datos de usuarios y pagos en una app nativa.
  • He integrado el flujo completo: usuario añade productos al carrito, inicia sesión, procede al checkout, "paga" y es redirigido a una pantalla de confirmación con un ID de transacción simulado.
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión