Integración con API de Pagos y Autenticación

Lectura
25 min~12 min lectura
Objetivo de la lección

Hasta ahora, hemos construido interfaces, gestionado estados y navegado entre pantallas.

Puntos de control
  • Introducción: El Corazón de la Aplicación Comercial
  • Concepto Clave: Tokens, Pasarelas y la Analogía del Festival
  • Cómo Funciona en la Práctica: Arquitectura del Flujo
  • Código en Acción: Configuración y Autenticación

Introducción: El Corazón de la Aplicación Comercial

En el desarrollo de una aplicación de comercio electrónico, dos pilares sostienen toda la experiencia del usuario y la viabilidad del negocio: la autenticación y los pagos. Hasta ahora, hemos construido interfaces, gestionado estados y navegado entre pantallas. En esta lección, conectaremos nuestra aplicación con el mundo real, permitiendo que los usuarios se identifiquen de forma segura y realicen transacciones financieras. Esta integración transforma nuestro prototipo en un producto funcional y listo para generar valor.

Utilizaremos Expo y React Native para crear un flujo robusto. Para la autenticación, implementaremos un sistema basado en tokens JWT (JSON Web Tokens) con un backend RESTful. Para los pagos, integraremos Stripe, una de las plataformas más populares y seguras, a través de su SDK para React Native. La elección de Stripe se debe a su documentación excelente, su amplia aceptación global y su conjunto de herramientas diseñadas específicamente para desarrolladores móviles, lo que nos permite evitar la complejidad de manejar datos sensibles de tarjetas directamente en nuestra app.

El objetivo final es crear un flujo de usuario cohesivo: registro/login → navegación en la app autenticada → construcción de un carrito → checkout con pago seguro → confirmación. Cada paso debe ser seguro, manejando errores de red y validaciones de forma elegante, proporcionando retroalimentación clara al usuario en todo momento.

Concepto Clave: Tokens, Pasarelas y la Analogía del Festival

Imagina que estás en un gran festival de música. Al llegar, compras tu entrada (te registras). El personal te verifica la identidad y te coloca una pulsera especial (autenticación). Esa pulsera es tu token de acceso. Mientras esté en tu muñeca, puedes entrar y salir de las diferentes áreas (pantallas de la app), comprar comida y bebida (interactuar con recursos protegidos) sin tener que mostrar tu identificación cada vez. Si la pulsera se rompe o el festival termina (el token expira), pierdes tus privilegios y debes obtener una nueva (hacer login o refresh).

Ahora, quieres comprar una camiseta del evento. Vas a la tienda, eliges tu talla y al pagar, no le das tu dinero en efectivo al vendedor. En su lugar, te diriges a una cabina oficial del banco (pasarela de pago) dentro del recinto, como Stripe. Tú le das tus datos de pago únicamente al banco, que es un experto en seguridad. El banco le da al vendedor un recibo de confirmación (token de pago o PaymentIntent) que dice "el pago fue exitoso", sin revelar nunca tu información financiera. El vendedor confía en ese recibo y te entrega la camiseta. Nuestra app actúa como el vendedor: nunca ve o almacena los datos de la tarjeta, solo interactúa con los tokens de confirmación de la pasarela.

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

El flujo se divide en dos procesos principales que, aunque distintos, a menudo se entrelazan. Para la autenticación, seguiremos este patrón: 1) El usuario ingresa email y contraseña en la app. 2) La app envía estas credenciales (por HTTPS) a nuestro endpoint de login en el backend. 3) El backend verifica contra la base de datos, y si son correctas, genera un JWT y lo devuelve. 4) La app recibe el JWT y lo almacena de forma segura (usando, por ejemplo, AsyncStorage o SecureStore de Expo). 5) Para cada petición posterior a un endpoint protegido (como "mis pedidos"), la app adjunta ese JWT en el encabezado Authorization.

Para los pagos con Stripe, el flujo es un poco más elaborado y requiere coordinación entre nuestra app, nuestro backend y los servidores de Stripe. El patrón más seguro y recomendado es: 1) La app informa a nuestro backend la intención de pagar un monto específico. 2) Nuestro backend crea un PaymentIntent en Stripe (usando la clave secreta de Stripe) y devuelve el client_secret de ese PaymentIntent a la app. 3) La app, usando el SDK de Stripe React Native, recoge los datos de la tarjeta del usuario a través de un componente seguro (CardField). 4) El SDK de Stripe en la app usa el client_secret para confirmar el pago directamente con los servidores de Stripe (usando una clave pública). 5) Stripe notifica a nuestro backend del resultado (éxito o fracaso) a través de un webhook. 6) Nuestro backend actualiza el estado del pedido en nuestra base de datos.

Tip Crítico: Nunca, bajo ninguna circunstancia, incrustes las claves secretas de Stripe (las que empiezan por 'sk_') en tu aplicación móvil. Cualquiera que inspeccione el paquete de la app podría robarlas y hacer cargos fraudulentos. La clave secreta solo debe vivir en tu backend seguro. La app solo usa la clave pública ('pk_').

Código en Acción: Configuración y Autenticación

Comencemos instalando las dependencias necesarias. Usaremos `axios` para las peticiones HTTP, `@react-native-async-storage/async-storage` para almacenar el token, y `@stripe/stripe-react-native` para los pagos.


expo install axios @react-native-async-storage/async-storage
expo install @stripe/stripe-react-native

Primero, implementemos el contexto de autenticación. Este será el encargado de gestionar el estado global del usuario y el token.


// contexts/AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import api from '../services/api'; // Axios configurado con la base URL

const AuthContext = createContext({});

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

  useEffect(() => {
    // Intentar cargar los datos del usuario desde el almacenamiento al iniciar
    loadStoredData();
  }, []);

  async function loadStoredData() {
    const storedUser = await AsyncStorage.getItem('@App:user');
    const storedToken = await AsyncStorage.getItem('@App:token');

    if (storedUser && storedToken) {
      setUser(JSON.parse(storedUser));
      // Configurar el token por defecto en axios para todas las peticiones futuras
      api.defaults.headers.common['Authorization'] = `Bearer ${storedToken}`;
    }
    setLoading(false);
  }

  async function signIn(email, password) {
    try {
      const response = await api.post('/auth/login', { email, password });
      const { user, token } = response.data;

      setUser(user);
      api.defaults.headers.common['Authorization'] = `Bearer ${token}`;

      // Persistir datos
      await AsyncStorage.setItem('@App:user', JSON.stringify(user));
      await AsyncStorage.setItem('@App:token', token);

      return { success: true };
    } catch (error) {
      return { success: false, error: error.response?.data?.message || 'Error de conexión' };
    }
  }

  async function signOut() {
    await AsyncStorage.clear();
    setUser(null);
    delete api.defaults.headers.common['Authorization'];
  }

  return (
    <AuthContext.Provider value={{ signed: !!user, user, loading, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
};

export function useAuth() {
  const context = useContext(AuthContext);
  return context;
}

Ahora, veamos un ejemplo de una pantalla de login que utiliza este contexto.


// screens/LoginScreen.js
import React, { useState } from 'react';
import { View, TextInput, Button, Text, Alert } from 'react-native';
import { useAuth } from '../contexts/AuthContext';

const LoginScreen = ({ navigation }) => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { signIn, loading } = useAuth();

  const handleLogin = async () => {
    if (!email || !password) {
      Alert.alert('Error', 'Por favor, completa todos los campos.');
      return;
    }
    const result = await signIn(email, password);
    if (!result.success) {
      Alert.alert('Error de inicio de sesión', result.error);
    }
    // Si es exitoso, el contexto actualizará el estado y la navegación (por ejemplo, un Stack Navigator) redirigirá automáticamente.
  };

  return (
    <View style={{ flex: 1, justifyContent: 'center', padding: 20 }}>
      <Text>Correo Electrónico</Text>
      <TextInput
        value={email}
       
        placeholder="[email protected]"
        autoCapitalize="none"
        keyboardType="email-address"
        style={{ borderWidth: 1, marginBottom: 15, padding: 10 }}
      />
      <Text>Contraseña</Text>
      <TextInput
        value={password}
       
        placeholder="********"
        secureTextEntry
        style={{ borderWidth: 1, marginBottom: 25, padding: 10 }}
      />
      <Button title={loading ? "Cargando..." : "Iniciar Sesión"} disabled={loading} />
      <Button title="Registrarse" => navigation.navigate('Register')} color="gray" />
    </View>
  );
};

export default LoginScreen;

Código en Acción: Integración de Pagos con Stripe

Ahora, integremos Stripe en la pantalla de checkout. Asumimos que ya tenemos un carrito con un total y que hemos obtenido un `clientSecret` de nuestro backend.


// screens/CheckoutScreen.js
import React, { useState, useEffect } from 'react';
import { View, Alert, ActivityIndicator } from 'react-native';
import { useStripe, CardField, useConfirmPayment } from '@stripe/stripe-react-native';
import api from '../services/api';
import { useCart } from '../contexts/CartContext'; // Suponiendo un contexto para el carrito

const CheckoutScreen = () => {
  const { initPaymentSheet, presentPaymentSheet } = useStripe();
  const [loading, setLoading] = useState(false);
  const [clientSecret, setClientSecret] = useState(null);
  const { totalAmount, items } = useCart();

  // 1. Crear el PaymentIntent en el backend al montar la pantalla
  useEffect(() => {
    fetchPaymentIntent();
  }, []);

  const fetchPaymentIntent = async () => {
    setLoading(true);
    try {
      const response = await api.post('/payments/create-payment-intent', {
        amount: totalAmount * 100, // Stripe trabaja en centavos/mínima unidad monetaria
        currency: 'usd',
        metadata: { cartItems: JSON.stringify(items) }
      });
      setClientSecret(response.data.clientSecret);
    } catch (error) {
      Alert.alert('Error', 'No se pudo preparar el pago.');
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  // 2. Método para manejar el pago
  const handlePayPress = async () => {
    if (!clientSecret) {
      Alert.alert('Error', 'Información de pago no disponible.');
      return;
    }
    setLoading(true);
    try {
      // 3. Inicializar la hoja de pago de Stripe (método recomendado para un mejor UX)
      const { error } = await initPaymentSheet({
        paymentIntentClientSecret: clientSecret,
        merchantDisplayName: 'Mi Tienda, S.A.',
      });

      if (error) {
        Alert.alert(`Error código: ${error.code}`, error.message);
        setLoading(false);
        return;
      }

      // 4. Presentar la hoja de pago al usuario
      const { error: presentError } = await presentPaymentSheet();

      if (presentError) {
        Alert.alert(`Pago fallido: ${presentError.code}`, presentError.message);
      } else {
        Alert.alert('¡Éxito!', 'Tu pago fue confirmado.');
        // Aquí navegarías a una pantalla de confirmación y vaciarías el carrito.
      }
    } catch (e) {
      console.error(e);
      Alert.alert('Error inesperado', 'Ocurrió un problema durante el pago.');
    } finally {
      setLoading(false);
    }
  };

  if (loading && !clientSecret) {
    return (
      <View style={{ flex: 1, justifyContent: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text style={{ fontSize: 20, marginBottom: 20 }}>Total a pagar: ${totalAmount.toFixed(2)}</Text>
      
      <Button
        title={loading ? "Procesando..." : "Pagar Ahora"}
       
        disabled={loading || !clientSecret}
      />
    </View>
  );
};

export default CheckoutScreen;

Tip de UX: El método `initPaymentSheet` y `presentPaymentSheet` proporciona una interfaz de pago nativa y optimizada que Stripe gestiona por completo. Es más seguro y tiene una tasa de conversión más alta que implementar un `CardField` manual, ya que guía al usuario paso a paso y puede incluir Apple Pay/Google Pay automáticamente.

Errores Comunes y Cómo Evitarlos

1. Almacenar el Token JWT en un Estado Simple sin Persistencia: Si solo guardas el token en el estado de React (useState), se perderá al recargar la app. El usuario tendrá que iniciar sesión constantemente. Solución: Siempre persiste el token y los datos básicos del usuario en un almacenamiento seguro como `AsyncStorage` o `SecureStore`, y recupéralos al iniciar la aplicación, como se muestra en el `AuthProvider`.

2. Enviar la Clave Secreta de Stripe desde el Frontend: Este es un error de seguridad grave. La clave secreta debe estar solo en tu backend. En el frontend solo se usa la clave pública para inicializar el SDK y el `client_secret` que el backend genera para una transacción específica. Solución: Nunca escribas `sk_` en el código de tu app móvil. Todo el proceso de creación del `PaymentIntent` debe ser una llamada a tu API.

3. No Manejar Estados de Carga y Error en la UI: Dejar al usuario frente a una pantalla congelada mientras se hace una petición de login o pago genera una mala experiencia y puede causar toques múltiples que dupliquen acciones. Solución: Siempre utiliza estados de `loading` para deshabilitar botones y mostrar indicadores de actividad (`ActivityIndicator`). Muestra Alertas o mensajes inline para los errores, dándole contexto al usuario (ej., "Contraseña incorrecta", "Error de red").

4. Olvidar los Webhooks de Stripe: Confiar únicamente en la respuesta del frontend para marcar un pedido como "pagado" es riesgoso. La conexión del usuario puede fallar después del pago exitoso pero antes de que tu backend lo registre. Solución: Configura webhooks en el dashboard de Stripe que apunten a un endpoint en tu backend (ej., `/webhooks/stripe`). Stripe notificará a tu backend de eventos como `payment_intent.succeeded`. Tu backend debe verificar la firma del webhook y luego actualizar el estado del pedido en tu base de datos. Es la fuente de verdad.

5. Validación Insuficiente en el Backend: Asumir que las peticiones que llegan con un token JWT son siempre legítimas. Un atacante podría modificar el `client_secret` o el `amount` en la petición desde la app si no hay validación del lado del servidor. Solución: En el endpoint `/payments/create-payment-intent`, el backend debe calcular el monto a cobrar basándose en el carrito del usuario (almacenado en la sesión del backend o en la base de datos, vinculado al ID de usuario del JWT), no confiar en el monto que envía el frontend.

Checklist de Dominio

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

  • Puedo explicar la diferencia entre un token de autenticación JWT y un client_secret de Stripe, y dónde debe residir cada uno (frontend/backend).
  • He implementado un Contexto de Autenticación en React Native que maneja login, persistencia de sesión y logout correctamente.
  • Sé cómo adjuntar automáticamente el token JWT a los encabezados de todas las peticiones HTTP salientes usando Axios Interceptors o configuración por defecto.
  • He integrado el SDK de Stripe React Native en un proyecto Expo y he inicializado correctamente con la clave pública.
  • Puedo describir el flujo completo de un pago: desde que el usuario toca "pagar" hasta que mi base de datos registra el pedido como completado, incluyendo el rol del webhook.
  • En mi pantalla de checkout, manejo correctamente los estados de carga, éxito y error, proporcionando retroalimentación clara al usuario.
  • He configurado (o comprendo la necesidad de) un endpoint de webhook en mi backend para escuchar eventos de Stripe y actualizar el estado de los pedidos.
  • Puedo listar al menos tres errores de seguridad comunes en esta integración y las prácticas para mitigarlos.
Falar no WhatsApp
Laboratorio de práctica

Antes de marcar esta lección como completa, escribí una evidencia breve para Desarrollo de Apps Nativas con React Native y Expo: De Cero a Producción: un ejemplo, una decisión, una captura, una mini demo o una nota que puedas reutilizar en portfolio.

Reflexión rápida

¿Qué cambiarías en tu forma de trabajar después de aplicar integración con api de pagos y autenticación?

De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

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

Integración con API de Pagos y Autenticación | Cursalo