I have some issues setting up Stripe payment in my Medusa-JS Next JS frontend.
When entering card details in the checkoutForm
the entire StripePayment
re-renders all the time when clicking on anything inside my form that is not the PaymentElement
.
I have a suspicion that it is due to changes in my Stripe component’s useEffect but I cannot see why it should be updated inside my form.
"use client";
import { useState, useEffect } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";
import CheckoutForm from "./form";
const STRIPE_PK_KEY = process.env.NEXT_PUBLIC_STRIPE_PK_KEY;
const StripePayment = ({ cart }) => {
const [stripePromise, setStripePromise] = useState(null);
const [clientSecret, setClientSecret] = useState();
useEffect(() => {
if (!STRIPE_PK_KEY && !cart && !cart.payment_sessions) {
return null;
}
const isStripeAvailable = cart.payment_sessions?.some(
(session) => session.provider_id === "stripe"
);
if (!isStripeAvailable) {
return;
}
setStripePromise(loadStripe(STRIPE_PK_KEY));
setClientSecret(cart.payment_session.data.client_secret);
}, [cart.id]);
return (
<div>
<h1>Stripe payment</h1>
{stripePromise && clientSecret && (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm clientSecret={clientSecret} cart={cart} />
</Elements>
)}
</div>
);
};
export default StripePayment;
"use client";
import {
useElements,
useStripe,
PaymentElement,
} from "@stripe/react-stripe-js";
import { useState } from "react";
import medusaClient from "../../../lib/medusaClient";
export default function Form({ clientSecret, cart }) {
const stripe = useStripe();
const elements = useElements();
const [message, setMessage] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!stripe || !elements) {
console.log("No tripe or elements");
return;
}
setIsProcessing(true);
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: elements.getElement(PaymentElement),
billing_details: {
name: e.target.name.value,
email: e.target.email.value,
phone: e.target.phone.value,
address: {
city: cart.shipping_address.city,
country: cart.shipping_address.country,
line1: cart.shipping_address.line1,
line2: cart.shipping_address.line2,
postal_code: cart.shipping_address.postal_code,
},
},
},
}
);
if (error) {
setMessage(error.message);
} else if (paymentIntent && paymentIntent.status === "succeeded") {
setMessage("Payment succeeded");
try {
await medusaClient.carts.complete(cart.id);
console.log("Order completed");
} catch (err) {
console.error("Error completing order:", err);
}
} else {
setMessage("An unexpected error occurred");
}
console.log("Message: ", message);
setIsProcessing(false);
};
return (
<form id="payment-form" onSubmit={(e) => handleSubmit(e)}>
{/* INPUT LABELS OMITTED FOR SAKE OF READIBILITY */}
<PaymentElement id="payment-element" />
<button
className="px-4 py-2 mt-4 text-white bg-blue-500"
disabled={isProcessing || !stripe || !elements}
id="submit"
>
<span id="button-text">
{isProcessing ? "Processing ... " : "Pay now"}
</span>
</button>
{/* Show any error or success messages */}
{message && <div id="payment-message">{message}</div>}
</form>
);
}
There's some unusual things in your code I see:
confirmCardPayment
, which is used for when you are using the CardElement, if you're using the <PaymentElement>
you must use confirmPayment
insteaduseElements()
which provides the global Stripe instance that's already initialised and know the clientSecret. Maybe this extra dependency causes some of the re-render issue you see?The normal integration is more like this:
Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm cart={cart} />
</Elements>
CheckoutForm:
...
const { error } = await stripe.confirmPayment({
elements, // from useElements ; you do not need to pass the clientSecret
confirmParams: {
// Make sure to change this to your payment completion page
return_url: `${window.location.origin}/completion`,
payment_method_data: {
billing_details:{
name: e.target.name.value,
email: e.target.email.value,
...
}
}
},
});
...
I would try to rewrite your code to match Stripe's docs and see if that has an impact.