typescript - "href" matchade inte mellan servern och klienten i nextjs stripe checkout


så för att börja med, detta är min första fråga jag har behövt ställa på Stack Overflow, så ursäkta i förväg om jag inte ger tillräckligt med detaljer eller glömmer att inkludera vissa detaljer.

Jag har framgångsrikt implementerat ett anpassat betalningsflöde med Stripes API i min Next.js-applikation. Allt verkar fungera bra för själva kassan, men när Stripe omdirigerar till den angivna return_url, har Next.js server och klient olika URL-parametrar. Detta beror på att Stripe tillhandahåller URL-parametrarna till klienten vid omdirigering, men inte till servern, och därför uppstår detta fel:

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#"

Det inkluderade exempelapplikationen för Stripe ger inte detta server-/klientfel, exemplet kan hittas här: stripe custom payment flow application. Det finns ingen skillnad i koden som används i min applikation jämfört med det tillhandahållna exemplet, de är 1:1 kopior, förutom det faktum att exemplet använder projektets rot för dess retur-URL, istället för en next.js-sida, som min app använder checkout.tsx.

Jag är inte en erfaren utvecklare, så jag vet inte varför min app ger mig detta fel, när exempelappen inte ger fel (eller så lyckas jag inte återskapa det). Jag har undersökt detta fel noggrant, och de flesta lösningar kör bara koden på klienten genom att kontrollera om fönstret finns i next.js. Men det fungerar inte här eftersom stripe-biblioteket självt injicerar URL-parametrarna genom return_url, och jag har ingen kontroll över vad det injicerar, eller hur det injiceras.

Här är Next.js-sidan och komponenten som används i kassan:

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>
  );
}

All hjälp med att bli av med detta next.js mismatch-fel uppskattas mycket eftersom jag har fastnat på detta problem i flera dagar! Om du också har några råd/bästa praxis för hantering av returnerings-URL-flödet, skulle det vara till stor hjälp eftersom Stripe-dokumentationen inte ger mycket detaljer.

Tack!