Búsqueda de sitios web

typescript - "href" no coincidió entre el servidor y el cliente en el pago de Stripe de Next.js


Así que solo para empezar, esta es la primera pregunta que he tenido que hacer en Stack Overflow, así que disculpas de antemano si no proporciono suficiente detalle o si olvido incluir ciertos detalles.

He implementado con éxito un flujo de pago personalizado usando la API de Stripe en mi aplicación next.js. Todo parece funcionar bien para el proceso de pago real, sin embargo, cuando Stripe redirige a la return_url especificada, el servidor y el cliente de next.js tienen diferentes parámetros de URL. Esto se debe a que Stripe proporciona los parámetros de URL al cliente al redirigir, pero no al servidor, de ahí que ocurra este error:

Warning: Prop `href` did not match. Server: "/checkout#" Client: "/checkout?payment_intent=pi_3La0akIazEKeaf0C29fCT4rX&payment_intent_client_secret=pi_3La0akIazEKeaf0C29fCT4rX_secret_kzvRJcHab0DDdktG7EyCkDJPJ&redirect_status=succeeded#"

La aplicación de ejemplo de Stripe incluida no produce este error de desajuste entre servidor/cliente, el ejemplo se puede encontrar aquí: aplicación de flujo de pago personalizado de Stripe. No hay diferencia en el código utilizado en mi aplicación en comparación con el ejemplo proporcionado, son copias 1:1, aparte del hecho de que el ejemplo usa la raíz del proyecto para su URL de retorno, en lugar de una página de next.js, que mi aplicación utiliza checkout.tsx.

No soy un desarrollador experimentado, por lo que no sé por qué mi aplicación me está dando este error, cuando la aplicación de ejemplo no produce el error (o no logro reproducirlo). He investigado este error extensamente, y la mayoría de las soluciones solo ejecutan el código en el cliente verificando si la ventana existe en next.js. Sin embargo, eso no funciona aquí ya que la biblioteca de Stripe inyecta los parámetros de URL a través del return_url, y no tengo control sobre lo que inyecta ni sobre cómo se inyecta.

Aquí están la página y el componente de Next.js utilizados en el pago:

checkout.tsx:

import React from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";

import CheckoutForm from "../components/CheckoutForm";

// Make sure to call loadStripe outside of a component’s render to avoid
// recreating the Stripe object on every render.
// This is your test publishable API key.
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

export default function Checkout() {
  const [clientSecret, setClientSecret] = React.useState("");

  React.useEffect(() => {
    // Create PaymentIntent as soon as the page loads
    fetch("/api/create-payment-intent", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items: [{ id: "xl-tshirt" }] }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, []);

  const appearance = {
    theme: "flat" as const,
    variables: {
      fontFamily: ' "Gill Sans", sans-serif',
      fontLineHeight: "1.5",
      fontSizeBase: "16px",
      borderRadius: "10px",
      colorBackground: "#F6F8FA",
      colorPrimaryText: "#262626",
    },
    rules: {
      ".Block": {
        backgroundColor: "var(--colorBackground)",
        boxShadow: "none",
        padding: "1px",
      },
      ".Input": {
        padding: "10px",
        fontSize: "14px",
      },
      ".Tab": {
        padding: "10px 12px 8px 12px",
        border: "none",
      },
      ".Tab:hover": {
        border: "none",
        boxShadow: "0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 7px rgba(18, 42, 66, 0.04)",
      },
      ".Tab--selected, .Tab--selected:focus, .Tab--selected:hover": {
        border: "none",
        backgroundColor: "#fff",
        boxShadow: "0 0 0 1.5px var(--colorPrimaryText), 0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 7px rgba(18, 42, 66, 0.04)",
      },
      ".Label": {
        fontWeight: "500",
      },
    },
  };

  const options = {
    clientSecret,
    appearance,
  };

  return (
    <div className="App">
      {clientSecret && (
        <Elements options={options} stripe={stripePromise}>
          <CheckoutForm />
        </Elements>
      )}
    </div>
  );
}

checkoutForm.tsx:

import React from "react";
import { PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";

export default function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const [message, setMessage] = React.useState(null);
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    if (!stripe) {
      return;
    }

    const clientSecret = new URLSearchParams(window.location.search).get("payment_intent_client_secret");

    if (!clientSecret) {
      return;
    }

    stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => {
      switch (paymentIntent.status) {
        case "succeeded":
          setMessage("Payment succeeded!");
          break;
        case "processing":
          setMessage("Your payment is processing.");
          break;
        case "requires_payment_method":
          setMessage("Your payment was not successful, please try again.");
          break;
        default:
          setMessage("Something went wrong.");
          break;
      }
    });
  }, [stripe]);

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (!stripe || !elements) {
      // Stripe.js has not yet loaded.
      // Make sure to disable form submission until Stripe.js has loaded.
      return;
    }

    setIsLoading(true);

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: { return_url: "http://localhost:3000/checkout" },
    });

    // This point will only be reached if there is an immediate error when
    // confirming the payment. Otherwise, your customer will be redirected to
    // your `return_url`. For some payment methods like iDEAL, your customer will
    // be redirected to an intermediate site first to authorize the payment, then
    // redirected to the `return_url`.
    if (error.type === "card_error" || error.type === "validation_error") {
      setMessage(error.message);
    } else {
      setMessage("An unexpected error occurred.");
    }

    setIsLoading(false);
  };

  return (
    <form id="payment-form" onSubmit={handleSubmit}>
      <PaymentElement id="payment-element" />
      <button disabled={isLoading || !stripe || !elements} id="submit">
        <span id="button-text">{isLoading ? <div className="spinner" id="spinner"></div> : "Pay now"}</span>
      </button>
      {/* Show any error or success messages */}
      {message && <div id="payment-message">{message}</div>}
    </form>
  );
}

Cualquier ayuda para deshacerse de este error de desajuste de Next.js es muy apreciada, ¡ya que he estado atrapado en este problema durante días! Si también tienes algún consejo/mejores prácticas sobre cómo manejar el flujo de URL de retorno, eso sería de gran ayuda, ya que la documentación de Stripe no da muchos detalles.

¡Gracias!