CodingPR

Conecta Stripe a un React TypeScript E-commerce

Conecte Stripe a un comercio electrónico con React y TypeScript.

Denisse Avatar

Por:

Denisse Abreu
Agosto 16, 2022 8pm ET

Eng/Spa 7-min

Hola 👋🏼, en este tutorial, te enseñaré cómo conectar Stripe a una tienda de compras React TypeScript. Para esta integración, deberá sentirse cómodo usando React y TypeScript y, por supuesto, tener un sitio web de comercio electrónico. Nuestro entorno de trabajo es React TypeScript, React Router Dom v6 y Express js como administrador de backend. Si no tiene un sitio web de comercio electrónico, puede dar fork a mi Mini Monster Store en mi repositorio de Github.

  • Demo de la integración

Configura tu cuenta de Stripe.

Para comenzar con Stripe, debe abrir una cuenta con Stripe; Después de abrir su cuenta, vaya a la sección de desarrolladores y acceda a sus claves API.
En la sección de desarrolladores, encontrarás dos claves: clave publicable (publishable key) y clave secreta (secret key); copie la clave secreta en el archivo .env de su servidor.

.env
STRIPE_SECRET_KEY=<TU-CLAVE-SECRETA>

Stripe dashboard

Conecte el servidor Express js.

Primero, comience instalando la librería de Stripe; Usaré Express js como servidor, pero puede usar su servidor preferido. Consulta la guía de Stripe para los diferentes lenguajes de servidor que puede utilizar.

Instalar a través de npm
npm install stripe --save

Pegue el siguiente código en su index.ts en la raíz del servidor Express js. Si no sabe cómo configurar un servidor Express js con TypeScript, vaya a mi tutorial Crea un proyecto React y Express js con TypeScript y siga la primera parte de cómo configurar Express js TypeScript.

index.ts
import express, { Express, Request, Response } from "express"; import path from "path"; import dotenv from "dotenv"; import cors from "cors"; dotenv.config(); const app: Express = express(); const port = process.env.PORT || 8000; const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); app.use(express.json()); app.use(cors()); interface RequestBody { amount: number } app.post('/secret', async (req: Request, res: Response) => { const { amount }:RequestBody = req.body; try { const intent = await stripe.paymentIntents.create({ amount: amount, currency: 'usd', automatic_payment_methods: {enabled: true}, }); res.json({ client_secret: intent.client_secret }); } catch (error) { console.log(error); } }); app.listen(port, () => { console.log(`Example app listening on port ${port}`) });
  • Cuando su servidor esté listo, abra su terminal y ejecute: npm run dev y comience a escuchar las Solicitudes de API del lado del cliente.

Configura la integración de Stripe y React.

  • Cree el archivo .env en la raíz del Mini Monster Store y copie su clave publicable de Stripe.
.env
REACT_APP_STRIPE_PUBLISHABLE_KEY=<TU-CLAVE-PUBLICABLE-DE-STRIPE>
  • Abra su terminal e instale la librería React Stripe.js.
Instalar a través de npm
npm install --save @stripe/react-stripe-js @stripe/stripe-js
  • Vaya a la carpeta Utils y cree la carpeta Stripe; dentro de esta carpeta cree payment-intent-utils.ts; este archivo se encargará de hacer la llamada API al servidor.
Git Bash
cd utils mkdir stripe cd stripe touch payment-intent-utils.ts
  • Pegue el siguiente código dentro del archivo payment-intent-utils.ts; Este archivo contiene la llamada Fetch() al servidor con la URL y el monto total de la transacción multiplicado por 100. Recuerda, Stripe no aceptará números decimales.
utils/stripe/payment-intent-utils.ts
export const paymentIntent = async <T>( url: string, cartTotal: number ) : Promise<T> => { const res = await fetch(url, { method: 'Post', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ amount: cartTotal*100 }) }); const {client_secret: clientSecret} = await res.json(); return await clientSecret; }

Vaya al subtotal card, importe la función paymentIntent() que se encuentra en la carpeta utils, y copie el try and catch dentro de la constante checkout. cuando el cliente haga clic en el botón subtotal, se disparará una llamada al servidor solicitando la clave secreta del cliente. Si la llamada tiene éxito, su cliente será redirigido al checkout route con el secreto del cliente como parámetro. Esta integración utiliza React Router para moverse fácilmente entre las páginas de la tienda de compras.

components/subtotal-card/subtotal-card.tsx
import { paymentIntent } from "../../utils/stripe/payment-intent-utils"; const checkout = async () => { try { const client_secret: unknown = await paymentIntent( 'http://127.0.0.1:8000/secret', cartTotal ) navigate('/checkout', { state: { client_secret } }) } catch (error) { alert('Se ha producido un error; inténtalo de nuevo más tarde!') } };

Abra el checkout route e importe la función de react-router-dom useLocation(). Esta función se encargará de obtener el secreto de cliente que enviamos desde el subtotal card; pase este client_secret como prop al checkout card.

routes/checkout.tsx
import { useContext } from "react"; import { useLocation } from "react-router-dom"; import { CartContext } from "../context/cart-context/cart-context"; import { CartItem } from "../context/cart-context/cart-types"; import SubTotalItem from "../components/subtotal-item/subtotal-item"; import CheckoutCard from "../components/checkout-card/checkout-card"; const Checkout = () => { const { cartItems } = useContext(CartContext); const params = useLocation(); const secret: any = params.state; return ( <div> <div className="container mx-auto mt-10"> <CheckoutCard secret={secret.client_secret} /> </div> <div className={(cartItems.length === 1 ? "" : "mobile:grid grid-cols-2 ") + "tablet:grid-cols-none"}> { cartItems.length ? cartItems.map((monster: CartItem) => <SubTotalItem key={monster.id} monster={monster} />) : <span className='flex justify-center items-center text-3xl font-bold'>Your cart is empty</span> } </div> </div> ) } export default Checkout;

Vaya a la carpeta components y cree la carpeta payment-form; dentro de esa carpeta, cree el archivo payment-form.tsx. Este archivo será el encargado de mostrar el Elemento de Pago Stripe al cliente. Hay dos componentes input en el formulario de pago solo para mostrar cómo recopilar datos del cliente; Si no sabe cómo hacer componentes input reusables, vaya a la tercera parte de mi tutorial Crea un proyecto React y Express js con TypeScript.

Git Bash
cd components mkdir payment-form cd payment-form touch payment-form.tsx
components/payment-form/payment-form.tsx
import { useState, FormEvent, ChangeEvent } from 'react'; import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js"; import Button from '../button/button'; // Cree un componente de input reusable import FormInput from '../form-input/form-input'; const defaultFormFields = { displayName: '', email: '' } const PaymentForm = () => { const elements = useElements(); const stripe = useStripe(); const [isProcessingPayment, setIsProcessingPayment ] = useState(false); const [errorMessage, setErrorMessage] = useState<string | undefined>(); const [formFields, setFormFields] = useState(defaultFormFields); const { displayName, email } = formFields; const paymentHandler = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!stripe || !elements) { return; } setIsProcessingPayment(true); const {error} = await stripe.confirmPayment({ elements, confirmParams: { // redirigir a la ruta thankyou return_url: 'http://localhost:3000/thankyou/', payment_method_data: { billing_details: { name: displayName, email: email, phone: '7873679090', address: { line1: 'Example Building #129', city: 'Carolina', state: 'PR', postal_code: '00987', country: 'US' } } } }, }); setIsProcessingPayment(false); if (error) { // Este punto solo se alcanzará si hay un error inmediatamente // confirmando el pago. Muestre el error a su cliente (por ejemplo, pago // incompleto) setErrorMessage(error.message); } else { // Su cliente será redirigido a su `return_url`. Si el pago usa un // método como iDEAL, su cliente será redirigido a un sitio intermediario // primero para autorizar el pago, luego redirigido a `return_url`. } } const handleChange = (event: ChangeEvent<HTMLInputElement>) => { const { name, value } = event.target setFormFields({...formFields, [name]: value }) } return ( <div className='mt-3'> <form onSubmit={paymentHandler}> {/* Recopila más información del cliente */} <FormInput label="Name" type="text" required name="displayName" value={displayName} onChange={handleChange} /> <FormInput label="Email" type="email" required name="email" value={email} onChange={handleChange} /> <PaymentElement /> {errorMessage && <div className='text-pink-500 p-2 rounded-md mt-2 bold bg-pink-100' > {errorMessage} </div>} {/* isLoading deshabilitará el botón en su primer clic. */} <Button isLoading={isProcessingPayment} type='submit' buttonClass='w-full' > Pay </Button> </form> </div> ) }; export default PaymentForm;

Ahora viene la parte más difícil; Vaya al checkout card e importe Elements, loadStripe y el PaymentForm; obtenga el client_secret que enviamos desde el checkout route y utilícelo para mostrarle a su cliente el elemento de pago de Stripe. Personaliza el Stripe Appearance para que coincida con su UI.

components/checkout-card/checkout-card.tsx
import { useContext, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { CartContext } from "../../context/cart-context/cart-context"; import { Elements } from '@stripe/react-stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import PaymentForm from "../payment-form/payment-form"; /* Type assertion para transferir su clave publicable de Stripe desde el archivo .env. Elimina esto si tienes problemas y pon tu clave publicable directamente en la función loadStripe. */ const nodeEnv: string = (process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY as string); const stripePromise = loadStripe(nodeEnv); export type StripeTypes = { clientSecret: string; appearance: { theme: "stripe", variables: { colorPrimary: string } } }; const CheckoutCard = ({ secret }:any) => { const { cartTotal } = useContext(CartContext); const navigate = useNavigate(); useEffect(() => { if (!stripePromise || !secret) { navigate('/') } }, [secret, navigate]) const options: StripeTypes = { // pasa el client secret clientSecret: secret, // Totalmente personalizable con el appearance API. appearance: { theme: 'stripe', variables: { colorPrimary: '#008b8b' } }, }; return ( <div className="relative mx-3 rounded-lg border shadow-sm tablet:w-80 tablet:float-right laptop:mx-20"> <div className="m-5"> <h5 className="mb-2 pb-3 border-b border-gray-300 text-center text-2xl font-bold tracking-tight text-gray-900">Checkout</h5> <div className="grid grid-cols-2 text-center"> <div> Subtotal: <div> <div> Shipping: </div> <div> Total: </div> </div> </div> <div> ${cartTotal} <div> <div> $0 </div> <div> ${cartTotal} </div> </div> </div> </div> {secret && <Elements stripe={stripePromise} options={options}> <PaymentForm /> </Elements> } </div> </div> ) } export default CheckoutCard;

Después de enviar el formulario de pago, debemos verificar el estado del pago de Stripe. Cree el archivo payment-status.ts y colóquelo dentro de la carpeta Stripe en utils. Este código ejecutará una serie de switch cases para establecer el mensaje de error correspondiente y lógica adicional como vaciar el carrito en una transacción exitosa o retroceder a la página anterior si hay un error.

Git Bash
cd utils cd stripe touch payment-status.ts
utils/stripe/payment-status.ts
import {useState, useEffect, useContext} from 'react'; import { useNavigate } from 'react-router-dom'; import { useStripe } from '@stripe/react-stripe-js'; import { CartContext } from '../../context/cart-context/cart-context'; const PaymentStatus = () => { const navigate = useNavigate(); const stripe = useStripe(); const { clearCart } = useContext(CartContext); const [message, setMessage] = useState<any | null>(null); useEffect(() => { if (!stripe) { return; } // Recupere el parámetro "payment_intent_client_secret" adjunto a // su return_url por Stripe.js const clientSecret: any = new URLSearchParams(window.location.search).get( 'payment_intent_client_secret' ); stripe .retrievePaymentIntent(clientSecret) .then(({paymentIntent}: any) => { switch (paymentIntent.status) { case 'succeeded': clearCart(); setMessage(`¡Gracias! tu compra por $${paymentIntent.amount / 100} ha sido aceptada con éxito.`); break; case 'processing': setMessage("Procesando pago. Te informaremos cuando se reciba el pago."); break; case 'requires_payment_method': // Redirige a tu cliente a la página de checkout para intentar cobrar otra vez navigate(-1); setMessage('Pago fallido. Intente con otro método de pago.'); break; default: navigate(-1); setMessage('Algo salió mal.'); break; } }); }, [stripe, clearCart, navigate]); return message; }; export default PaymentStatus;

La última parte, ¡me alegro que hayas llegado hasta aquí! Vaya al thankyou route e importe la función payment status y colóquela dentro del componente Stripe Elements; hice una alerta sencilla para mostrar al cliente el estado del pago; Puedes hacer algo diferente aquí, ¡hasta la próxima!

routes/thankyou.tsx
import { Elements } from '@stripe/react-stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import PaymentStatus from '../utils/stripe/payment-status'; const nodeEnv: string = (process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY as string); const stripePromise = loadStripe(nodeEnv); const Thankyou = () => { return ( <Elements stripe={stripePromise}> <div className="container px-2 mx-auto laptop:px-40"> <div className="mx-auto m-5 mt-12 p-4 text-xl text-gray-700 border-l-4 border border-green-400 bg-green-100" role="alert"> <svg className="inline-flex mr-2 mb-1 w-5" viewBox="0 0 24 24"> <path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" /> </svg> <PaymentStatus /> </div> </div> </Elements> ) }; export default Thankyou;