En este post se procederá a explicar cómo realizar una pasarela de pagos con: para frontend > ReactJS, para backend > Express.js (con Node.js) y, para la pasarela propiamente, la plataforma > Stripe.

React se usará principalmente para la visualización de contenido, establecimiento de información, el carrito de compra con los determinados elementos, etc… Y Express.js simplemente para que se puedan realizar de forma funcional las peticiones y se puedan mostrar dichas respuestas en la parte cliente.

Es importante resaltar que la plataforma de Stripe es muy amplia. Tiene numerosas funcionalidades, pero lo importante y lo que nos interesa de ella es que es gratuita para pruebas (para pagos reales y no virtuales cobran comisiones) y tiene su propia pasarela de pago (que, por tanto, no es necesario customizar nada en ese aspecto, lo que hace que sea una herramienta muy cómoda y útil).

Nota: También se usará Bootstrap para dar estilo al proyecto y la librería ‘react-router-dom‘ para establecer las rutas (redirecciones…) en la parte front. Como entorno de desarrollo el recomendable es Visual Studio Code. Debe de estar instalado previamente Node.js.

El aspecto en vista de ramas del proyecto|repositorio es el siguiente:

Nota: El código está en inglés.

— Backend —

— Frontend —

En la parte de backend se encuentra el archivo .js principal y los respectivos módulos instalados de Express.js (Node.js).

En la parte de frontend se encuentran los assets con las imágenes, los componentes, las vistas de las páginas, los archivos .js del carrito y los productos y el resto de elementos generados en la instalación.

Todos los elementos resultarán claves para el funcionamiento adecuado de la aplicación.

Nota: Destacar que el código para llegar a una solución como es la plataforma de pagos se puede realizar de distintas formas, lo mío solo es una solución más (la que más me convencia y se ajustaba a lo que quería, y que explicaré a lo largo del post).

Antes de empezar directamente con el proyecto se deberá de configurar Stripe para posteriormente poder realizar los pagos.

Lo primero, registrarse. https://dashboard.stripe.com/register

Una vez establecida la cuenta (pasando por el proceso de verificación) se podrá ver el menú principal.

De ahí nos interesará la clave secreta (donde aparece para desarrolladores) para más adelante establecerlo en la parte servidora como parte de la identificación.

Para los pagos también habrá que crear una empresa ficticia. Para realizarlo hay que ir a configuración de la cuenta y pinchar abajo en datos de la cuenta o ir a https://dashboard.stripe.com/settings/account.

Se rellenan los datos y deberá de quedar como en la captura que se muestra a continuación:

Antes de finalizar con Stripe también se deberá establecer los productos, la información de estos y su valor. Habrá que dirigirse al panel de arriba e ir a > Productos. Lógicamente lo ideal es que se establezcan los mismos que más adelante se hará a código.

Para crear nuevo producto se le da a añadir producto y se rellena toda la información de este, viéndose algo así.

Lo importante de este panel es una vez que se selecciona uno de los elementos, ya que hay coger su clave de API para enlazarlo en el código como su id.

Ahora sí, una vez hecho todo lo que había que hacer del apartado de Stripe se puede comenzar con el proyecto.

Se empezará creando una carpeta principal con 2 subcarpetas llamadas frontend y backend. No es necesario tener 2 carpetas divisorias (se puede crear un proyecto React y fuera de este la parte de Express), pero yo lo recomiendo así.

Una vez situados con la terminal sobre la carpeta de frontend se introducirán los siguientes comandos para la creación del proyecto e instalaciones:

npx create-react-app tunombredeproyecto (stripe_project el mío)

npm install bootstrap react-bootstrap

npm install react-router-dom

npm start (para el arranque)

Se debe de quitar <React.StrictMode> en index.js para evitar posibles conflictos.

Se crea assets/images donde se almacenarán las imágenes de los productos.

Se crean las carpetas components para los componentes y pages para las vistas.

Sobre el directorio principal se crea productos con formato .js.

Nota: El formato de los componentes, vistas… serán como funciones que se exportarán y no como clases (sin constructores además).

[En Products.js] Para los productos se recogen las imágenes y se crea un array de objetos con su información. También habrá una función para recoger la información de los objetos por Id. Estas funciones se exportarán.


import AcerNitro5Image from "./assets/images/acer-nitro-5.jpg";
import AsusROGStrixG17Image from "./assets/images/asus-rog-strix-g17.jpg";
import AsusROGZephyrusM16Image from "./assets/images/asus-rog-zephyrus-m16.jpg";
import LenovoIdeapadGaming3Image from "./assets/images/lenovo-ideapad-gaming-3.jpg";
import MsiGF63ThinImage from "./assets/images/msi-gf63-thin.jpg";
import RazerBlade17Image from "./assets/images/razer-blade-17.jpg";

const arrayProducts = [
  {
    id: "price_1M8udcJ3u0rP0PhpqmbRutTf",
    name: "Acer Nitro 5",
    price: 829.99,
    image: AcerNitro5Image,
  },
  {
    id: "price_1M8uf5J3u0rP0PhpgzgGL2k2",
    name: "Asus ROG Strix G17",
    price: 2659,
    image: AsusROGStrixG17Image,
  },
  {
    id: "price_1M8ufUJ3u0rP0PhpblBNKOkW",
    name: "Asus ROG Zephyrus M16",
    price: 2839,
    image: AsusROGZephyrusM16Image,
  },
  {
    id: "price_1M8ufwJ3u0rP0Php0iRvz5Rs",
    name: "Lenovo IdeaPad Gaming 3",
    price: 829.19,
    image: LenovoIdeapadGaming3Image,
  },
  {
    id: "price_1M8ugHJ3u0rP0PhpJZtUvM2F",
    name: "MSI GF63 Thin",
    price: 949,
    image: MsiGF63ThinImage,
  },
  {
    id: "price_1M8ugcJ3u0rP0PhpCldvaoZv",
    name: "Razer Blade 17",
    price: 2299,
    image: RazerBlade17Image,
  },
];

function getProductData(id) {
  let productData = arrayProducts.find((product) => product.id === id);

  if (productData == undefined) {
    console.log("Product not found for id: " + id);
    return undefined;
  }

  return productData;
}

export { arrayProducts, getProductData };

[Sobre la carpeta pages] Se crean los archivos Cancel.js (para cuando se cancele compra), Success.js (para cuando se realice la compra satisfactoriamente) y Store.js (la principal en la que se verán todos los objetos).

[En App.js] Se establece el sistema de rutas importando Browser, Routes, Route; Bootstrap y todos las pages. Con el router se lleva a las vistas.

Nota: CartProvider se explicará más adelante con la parte del carrito. Se puede dejar comentado.

import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap.bundle";
import Navbar from "./components/Navbar";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Store from "./pages/Store";
import Success from "./pages/Success";
import Cancel from "./pages/Cancel";
import CartProvider from "./Cart";

function App() {
  return (
    <div>
      <CartProvider>
        <Navbar />
        <div className="container">
          <BrowserRouter>
            <Routes>
              <Route path="/" element={<Store />} />
              <Route path="/success" element={<Success />} />
              <Route path="/cancel" element={<Cancel />} />
            </Routes>
          </BrowserRouter>
        </div>
      </CartProvider>
    </div>
  );
}

export default App;

[En components] se crea la navbar con Navbar.js (que se ha importado en el anterior paso para siempre mostrarse), Product.js y CartProduct.js.

[Volviendo a pages] Se crean los mensajes de cancel y success, junto a un botón que llevará de vuelta a inicio.

function Cancel() {
  return (
    <div>
      <h1 className="mt-3">Purchase cancelled!</h1>
      <a href="/" className="btn btn-danger mt-2">
        Back to home
      </a>
    </div>
  );
}

export default Cancel;
function Success() {
  return (
    <div>
      <h1 className="mt-3">Thanks for your purchase!</h1>
      <a href="/" className="btn btn-success mt-2">
        Back to home
      </a>
    </div>
  );
}

export default Success;

[En pages > Store.js] Se muestran los productos recogidos de Products.js con un import y recorriéndolos, y se pasan al componente llamado product (que vendría a ser el hijo) con otro import de la siguiente manera:

import { arrayProducts } from "../Products";
import Product from "../components/Product";

function Store() {
  return (
    <div>
      <h1 className="mt-3">Main page store</h1>
      <div className="row g-4 mt-2 mb-4">
        {arrayProducts.map((product, index) => (
          <div className="col-md-4" key={index}>
            <Product product={product}/>
          </div>
        ))}
      </div>
    </div>
  );
}

export default Store;

[Sobre el directorio principal] Al nivel de Products.js, se crea Cart.js que contendrá las funciones del carrito sobre los elementos ya creados.

Se dispondrá fundamentalmente de 1 array llamado items y 5 funciones.

Funciones:

Obtener cantidad > getQuantity()

Añadir elemento > addItem()

Eleminar un elemento > removeItem()

Eliminar definitivamente un elemento > deleteItem()

Obtener el precio total > getTotal()

Se importa nuevamente las funciones de los productos.

Para tener de forma global los elementos (sin tener que usar «props«) se usa una propiedad de React llamada createContext que se importará junto a useState para la declaración de variables (y su estado).

Los valores del context se recogen creando un «provider» al cual se le transmite un hijo adicionalmente que vendrá a ser el valor de la función realizada.

A las funciones se les determina por parámetro el id del elemento seleccionado y en base a lo deseado se establecen las condiciones, ya sea de añadirlo, borrarlo y demás…

Nota: Como ya mencionaba antes la lógica puede variar el código perfectamente y hasta complejizarse en base a lo que se desee realizar u otros numerosos factores por ejemplo.

import { createContext, useState } from "react";
import { arrayProducts, getProductData } from "./Products";

export const Cart = createContext({
  items: [],
  getQuantity: () => {},
  addItem: () => {},
  removeItem: () => {},
  deleteItem: () => {},
  getTotal: () => {},
});

export function CartProvider({ children }) {
  const [cartProducts, setCartProducts] = useState([]);
  
  function getQuantity(id) {
    const quantity = cartProducts.find(
      (product) => product.id === id
    )?.quantity;

    if (quantity === undefined) {
      return 0;
    }

    return quantity;
  }

  function addItem(id) {
    const quantity = getQuantity(id);

    if (quantity === 0) {
      setCartProducts([...cartProducts, { id: id, quantity: 1 }]);
    } else {
      setCartProducts(
        cartProducts.map((product) =>
          product.id === id ? { ...product, quantity: quantity + 1 } : product
        )
      );
    }
  }

  function removeItem(id) {
    const quantity = getQuantity(id);

    if (quantity === 1) {
      deleteItem(id);
    } else {
      setCartProducts((cartProducts) =>
        cartProducts.map((product) =>
          product.id === id ? { ...product, quantity: quantity - 1 } : product
        )
      );
    }
  }

  function deleteItem(id) {
    setCartProducts((cartProducts) =>
      cartProducts.filter((currentProduct) => currentProduct.id !== id)
    );
  }

  function getTotal() {
    let total = 0;

    cartProducts.map((cartItem) => {
      const productData = getProductData(cartItem.id);
      total += productData.price * cartItem.quantity;
    });

    return total;
  }

  const contextValue = {
    items: cartProducts,
    getQuantity,
    addItem,
    removeItem,
    deleteItem,
    getTotal,
  };
  return <Cart.Provider value={contextValue}>{children}</Cart.Provider>;
}

export default CartProvider;

[En components > Product.js] Mostraremos la información de los productos con lo que se le pasó por props de Store.js.

Además se importará Cart y useContext para usar las funciones que se habían creado. Se podrá añadir un producto y solicitarle, en el caso de haber cantidad, que se pueda añadir más o simplemente eliminar uno o todo el producto.

import { Cart } from "../Cart";
import { useContext } from "react";

function Product(props) {
  const { product } = props;
  const cart = useContext(Cart);
  const quantity = cart.getQuantity(product.id);

  return (
    <div className="card mb-2 h-100">
      <img
        src={product.image}
        className="card-img-top img-fluid w-75"
        alt={product.name}
      />
      <div className="card-body">
        <h5 className="card-title">{product.name}</h5>
        <p className="card-text">{product.price}€</p>
        {quantity > 0 ? (
          <div className="row">
            <div className="row m-auto">
              <div className="col-6">In cart: {quantity}</div>
              <div className="col-6">
                <button
                  className="btn btn-primary mx-2"
                  onClick={() => cart.addItem(product.id)}
                >
                  +
                </button>
                <button
                  type="button"
                  className="btn btn-primary mx-2"
                  onClick={() => cart.removeItem(product.id)}
                >
                  -
                </button>
              </div>
              <button
                type="button"
                className="btn btn-danger w-75 mt-4 m-auto"
                onClick={() => cart.deleteItem(product.id)}
              >
                Remove from cart
              </button>
            </div>
          </div>
        ) : (
          <button
            className="btn btn-primary"
            onClick={() => cart.addItem(product.id)}
          >
            Add to cart
          </button>
        )}
      </div>
    </div>
  );
}

export default Product;

[En components > Navbar.js] Con todos los elementos ya creados se tendrá una barra de navegación con un carrito que se abrirá mediante modal con todos los productos añadidos (o vacío).

Se vuelven a hacer los imports correspondientes. Por otro lado, se hará un conteo de los productos que se vayan almacenando en el carrito mediante un filtro al array y se volverá a establecer una conexión padre a hijo con props a CartProduct.js (que se debe importar también) al mostrarse el carrito.

[En components > CartProduct.js]

Une vez más se añaden los imports y con los props se declaran los productos y su información, junto a un botón que permitirá eliminar los productos.

import { Cart } from "../Cart";
import { useContext } from "react";
import { getProductData } from "../Products";

function CartProduct(props) {
  const cart = useContext(Cart);
  const id = props.id;
  const quantity = props.quantity;
  const productData = getProductData(id);

  return (
    <div>
      <h5 className="" style={{color: "purple"}}>
        {productData.name}
      </h5>
      <h6>
        Quantity: {quantity}
      </h6>
      <p>
      {(quantity * productData.price).toFixed(2).toString().replace(".", ",").replace(/\,00/,'')}€
      </p>
      <button className="btn btn-warning" onClick={() => cart.deleteItem(id)}>
        Remove from cart
      </button>
      <hr />
    </div>
  );
}

export default CartProduct

[Volviendo a Navbar.js] Se añade el bloque de ‘checkout’ que servirá para enviar los productos a Stripe para poder llevar a la pasarela (siempre y cuando la respuesta sea correcta).

import { useContext } from "react";
import { Cart } from "../Cart";
import CartProduct from "../components/CartProduct";

function Navbar() {
  const cart = useContext(Cart);
  const productsCount = cart.items.reduce(
    (sum, product) => sum + product.quantity,
    0
  );

  const checkout = async () => {
    await fetch("http://localhost:4000/checkout", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ items: cart.items }),
    })
      .then((response) => {
        console.log(response);
        return response.json();
      })
      .then((response) => {
        console.log(response.url);
        if (response.url) {
          window.location.assign(response.url);
        }
      });
  };

  return (
    <nav className="navbar navbar-expand-lg navbar-light bg-light">
      <div className="container-fluid">
        <a className="navbar-brand" href="/">
          Stripe Project
        </a>
        <button
          className="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarNav"
          aria-controls="navbarNav"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span className="navbar-toggler-icon"></span>
        </button>
        <div className="collapse navbar-collapse" id="navbarNav">
          <ul className="navbar-nav">
            <li className="nav-item">
              <a className="nav-link active" aria-current="page" href="/">
                Home
              </a>
            </li>
          </ul>
          <ul className="navbar-nav ms-auto">
            <button
              type="button"
              className="btn btn-dark"
              data-bs-toggle="modal"
              data-bs-target="#exampleModal"
            >
              Cart ({productsCount}) Items
            </button>

            <div
              className="modal fade"
              id="exampleModal"
              aria-labelledby="exampleModalLabel"
              aria-hidden="true"
            >
              <div className="modal-dialog">
                <div className="modal-content">
                  <div className="modal-header bg-dark text-white">
                    <h1 className="modal-title fs-5" id="exampleModalLabel">
                      Shopping Cart
                    </h1>
                    <button
                      type="button"
                      className="btn-close btn-close-white"
                      data-bs-dismiss="modal"
                      aria-label="Close"
                    ></button>
                  </div>
                  <div className="modal-body">
                    {productsCount > 0 ? (
                      <div>
                        {cart.items.map((product, index) => (
                          <CartProduct
                            id={product.id}
                            quantity={product.quantity}
                            key={index}
                          />
                        ))}

                        <h4>
                          Total:{" "}
                          {cart
                            .getTotal()
                            .toFixed(2)
                            .toString()
                            .replace(".", ",")
                            .replace(/\,00/, "")}
                          €
                        </h4>
                      </div>
                    ) : (
                      <h4 className="text-danger">Cart is empty</h4>
                    )}
                  </div>
                  <div className="modal-footer">
                    {productsCount > 0 ? (
                      <button
                        type="button"
                        className="btn btn-success"
                        onClick={checkout}
                      >
                        Checkout
                      </button>
                    ) : (
                      <button
                        type="button"
                        className="btn btn-secondary"
                        data-bs-dismiss="modal"
                      >
                        Close
                      </button>
                    )}
                  </div>
                </div>
              </div>
            </div>
          </ul>
        </div>
      </div>
    </nav>
  );
}

export default Navbar;

Por último, hay que salir del directorio frontend y colocarse ya sobre backend.

Se abre una nueva ventana de comandos (teniendo obviamente siempre el frontend encendido) y se introducen los siguientes comandos:

npm init –yes
npm install express cors stripe
npm start
(con el código)

Se crea el archivo fundamental server.js. Se establece express, cors y stripe para el correcto funcionamiento (en local) del contenido. Se debe indicar la contraseña privada de Stripe mencionada al inicio del post.

Se hace un «post» con el checkout del servidor, también asíncrono, y se recorre el array de elementos del carrito.

Después se envía a Stripe y de realizarse el pedido llevará a success que se estableció con rutas o a cancel de no realizarse.

Se recoge la url y el front lleva hasta la pasarela.

Por último, también para el correcto funcionamiento, es recomendado iniciar el backend en el puerto 4000.

const express = require("express");
var cors = require("cors");
const stripe = require("stripe")(
"tucodigoprivado"
);

const app = express();
app.use(cors());
app.use(express.static("public"));
app.use(express.json());

app.post("/checkout", async (req, res) => {
  const items = req.body.items;
  let arrayItems = [];
  items.forEach((item) => {
    arrayItems.push({
      price: item.id,
      quantity: item.quantity,
    });
  });

  const session = await stripe.checkout.sessions.create({
    line_items: arrayItems,
    mode: "payment",
    success_url: "http://localhost:3000/success",
    cancel_url: "http://localhost:3000/cancel",
  });

  res.send(
    JSON.stringify({
      url: session.url,
    })
  );
});

app.listen(4000, () => {
  console.log("Server is running on port 4000");
});

Con todo esto y ambas partes inicializadas ya estaría funcionando el proyecto y lo que se desea hacer.

Para simular un pago hay que usar la tarjeta que establece Stripe por defecto:

4242 4242 4242 4242 (con la fecha y CVC deseados). Para más métodos: https://stripe.com/docs/testing?locale=es-ES&testing-method=card-numbers

Después aparecerán los pagos con la cuenta en Stripe > Pagos.

Este proyecto de pasarela de pagos puede mejorarse por muchos otros lados. Se pueden crear diferentes mensajes emergentes para las acciones de la página, añadir sesiones al carrito, un login y registro de usuario, añadir gastos de envíos u otras funciones de Stripe…

En conclusión, puede resultar una aplicación algo compleja, pero no es difícil de entender una vez se profundiza en ella y se ven los enlaces entre sí. Sin duda es una gran herramienta de cara a simular una pasarela de pagos real y su entorno.

Capturas:

Autor: Andrei Garbea
Curso: Desarrollo Web Full Stack, MultiCloud y Multiplataforma
Centro: Tajamar
Año académico: 2022-2023
GithubEnlace a GitHub

Recursos:

https://reactjs.org/

https://reactrouter.com/en/main

https://react-bootstrap.github.io/getting-started/introduction

https://stripe.com/docs

https://expressjs.com/

https://nodejs.org/en/

Referencias:

https://www.youtube.com/watch?v=_8M-YVY76O8&ab_channel=TraversyMedia

Leave a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.