"use client"; /** * Stripe Payment Element 弹窗 * * 后端当前返回 PaymentIntent clientSecret,因此这里直接用它完成前端确认。 */ import { useId, useState, type FormEvent } from "react"; import { Elements, PaymentElement, useElements, useStripe, } from "@stripe/react-stripe-js"; import { loadStripe } from "@stripe/stripe-js"; import { ModalPortal } from "@/app/_components/core/modal-portal"; import { ExceptionHandler } from "@/core/errors"; import { ROUTES } from "@/router/routes"; import { Logger } from "@/utils/logger"; import { stripePaymentDialogStyles as styles } from "./stripe-payment-dialog.styles"; const log = new Logger("SubscriptionStripePaymentDialog"); const stripePublishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY; const stripePromise = stripePublishableKey ? loadStripe(stripePublishableKey) : null; export interface StripePaymentDialogProps { clientSecret: string; returnPath?: string; onClose: () => void; onConfirmed?: () => void; } export function StripePaymentDialog({ clientSecret, returnPath = ROUTES.subscription + "/success", onClose, onConfirmed, }: StripePaymentDialogProps) { const titleId = useId(); const descriptionId = useId(); if (!stripePromise) { return ( Payment unavailable Stripe publishable key is not configured for this build. OK ); } return ( Complete payment Enter your payment details securely through Stripe. ); } function StripePaymentForm({ returnPath, onClose, onConfirmed, }: Pick< StripePaymentDialogProps, "returnPath" | "onClose" | "onConfirmed" >) { const stripe = useStripe(); const elements = useElements(); const [errorMessage, setErrorMessage] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const handleLoadError = (event: Parameters< NonNullable["onLoadError"]> >[0]) => { log.error("[stripe-payment-dialog] PaymentElement load failed", { type: event.error.type, code: event.error.code, message: event.error.message, declineCode: event.error.decline_code, }); setErrorMessage( ExceptionHandler.message( event.error, "Payment methods could not be loaded. Please try again later.", ), ); }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (!stripe || !elements || isSubmitting) return; setIsSubmitting(true); setErrorMessage(null); const { error: submitError } = await elements.submit(); if (submitError) { setErrorMessage( ExceptionHandler.message(submitError, "Please check your payment info."), ); setIsSubmitting(false); return; } const returnUrl = new URL( returnPath ?? ROUTES.subscription + "/success", window.location.origin, ); const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: returnUrl.toString(), }, redirect: "if_required", }); if (error) { setErrorMessage( ExceptionHandler.message(error, "Payment confirmation failed."), ); setIsSubmitting(false); return; } onConfirmed?.(); }; return ( {errorMessage ? ( {errorMessage} ) : null} Cancel {isSubmitting ? "Processing..." : "Pay now"} ); }
Stripe publishable key is not configured for this build.
Enter your payment details securely through Stripe.
{errorMessage}