如何才能完成 Stripe 测试付款?

问题描述 投票:0回答:1

我犯了这个错误

IntegrationError:我们无法从指定元素检索数据。请确保您尝试使用的元素仍然已安装。

条纹交易显示付款但不完整,当我将鼠标悬停在工具提示上时,它显示

The customer has not entered their payment method.
参见图片。但我使用的是他们的测试卡号码,所以我不确定他们的意思

这里是结帐组件

import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import PageTitleHero from '../components/PageTitleHero';
import { useCart } from '../context/cartContext';
import { useAuth } from '../context/authContext'; 
import PropTypes from 'prop-types';
import Loading from '../components/Loading';
import UserInformation from '../components/checkout/UserInformation';
import StripePaymentForm from '../components/checkout/StripePaymentForm'; 

const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLIC_KEY);

const Checkout = () => {
  const { cart, setCart } = useCart();
  const { currentUser } = useAuth(); 
  const [useShippingAsBilling, setUseShippingAsBilling] = useState(true);
  const [formData, setFormData] = useState({});
  const [loading, setLoading] = useState(false); 
  const navigate = useNavigate();

  useEffect(() => {
    const savedCart = localStorage.getItem('cart');
    if (savedCart) {
      setCart(JSON.parse(savedCart));
    }

    if (currentUser) {
      setFormData({
        firstName: currentUser.firstName || '',
        lastName: currentUser.lastName || '',
        email: currentUser.email || '',
        address: currentUser.address || '',
        city: currentUser.city || '',
        state: currentUser.state || '',
        zip: currentUser.zip || '',
        'shipping-address': currentUser.shippingAddress || '',
        'shipping-city': currentUser.shippingCity || '',
        'shipping-state': currentUser.shippingState || '',
        'shipping-postal-code': currentUser.shippingPostalCode || '',
        'billing-address': currentUser.billingAddress || currentUser.shippingAddress || '',
        'billing-city': currentUser.billingCity || currentUser.shippingCity || '',
        'billing-state': currentUser.billingState || currentUser.shippingState || '',
        'billing-postal-code': currentUser.billingPostalCode || currentUser.shippingPostalCode || '',
      });
    }
  }, [setCart, currentUser]);

  const handleUseShippingAsBilling = (event) => {
    setUseShippingAsBilling(event.target.checked);
    setFormData((prevData) => ({
      ...prevData,
      'billing-address': event.target.checked ? prevData['shipping-address'] : '',
      'billing-city': event.target.checked ? prevData['shipping-city'] : '',
      'billing-state': event.target.checked ? prevData['shipping-state'] : '',
      'billing-postal-code': event.target.checked ? prevData['shipping-postal-code'] : '',
    }));
  };

  const handleFormChange = (event) => {
    const { name, value } = event.target;
    setFormData((prevData) => ({ ...prevData, [name]: value }));
  };

  const total = cart.reduce((sum, product) => sum + product.price * product.quantity, 0) + 30;

  if (loading) {
    return <Loading />;
  }

  if (cart.length === 0) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p className="text-lg font-semibold">Your cart is empty. Please add items to your cart before proceeding to checkout.</p>
      </div>
    );
  }

  return (
    <>
      <PageTitleHero text="Checkout" />
      <div className="relative mx-auto grid max-w-7xl grid-cols-1 gap-x-16 lg:grid-cols-2 lg:px-8 lg:pt-16">
        <OrderSummary total={total} cart={cart} />
        <PaymentAndShipping
          useShippingAsBilling={useShippingAsBilling}
          handleUseShippingAsBilling={handleUseShippingAsBilling}
          handleFormChange={handleFormChange}
          formData={formData}
          total={total}
          setLoading={setLoading}
          navigate={navigate}
          cart={cart}
        />
      </div>
    </>
  );
};

const OrderSummary = ({ total, cart }) => (
  <section aria-labelledby="summary-heading" className="py-12 md:px-10 lg:col-start-2 lg:row-start-1 lg:mx-auto lg:w-full lg:max-w-lg lg:bg-transparent lg:px-0 lg:pb-24 lg:pt-0">
    <div className="rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-black shadow-sm p-12">
      <h2 id="summary-heading" className="sr-only">Order summary</h2>
      <dl>
        <dt className="text-sm font-bold text-indigo-500">Amount due</dt>
        <dd className="mt-1 text-3xl font-bold tracking-tight text-black dark:text-white">${total.toFixed(2)}</dd>
      </dl>
      <ul role="list" className="divide-y divide-gray-200 dark:divide-gray-600 text-sm font-medium">
        {cart.map((product, index) => (
          <li key={index} className="flex items-start space-x-4 py-6">
            <img alt={product.name} src={product.imageSrc} className="h-20 w-20 flex-none rounded-md object-cover object-center bg-slate-200 dark:bg-slate-800" />
            <div className="flex-auto space-y-1">
              <h3 className="text-slate-800 dark:text-slate-300">{product.name}</h3>
              <p className="text-slate-500 dark:text-slate-400">{product.quantity} x ${product.price}</p>
            </div>
            <p className="flex-none text-base font-medium text-slate-800 dark:text-slate-300">${product.price * product.quantity}</p>
          </li>
        ))}
      </ul>
      <dl className="space-y-6 border-t border-gray-300 dark:border-gray-600 pt-6 text-sm font-medium">
        <div className="flex items-center justify-between">
          <dt>Subtotal</dt>
          <dd>${cart.reduce((sum, product) => sum + product.price * product.quantity, 0).toFixed(2)}</dd>
        </div>
        <div className="flex items-center justify-between">
          <dt>Shipping (Flat Rate)</dt>
          <dd>$30.00</dd>
        </div>
        <div className="flex items-center justify-between border-t border-gray-300 dark:border-gray-600 pt-6 font-bold text-black dark:text-white">
          <dt className="text-base">Total</dt>
          <dd className="text-base">${total.toFixed(2)}</dd>
        </div>
      </dl>
    </div>
  </section>
);

const PaymentAndShipping = ({ 
  useShippingAsBilling, 
  handleUseShippingAsBilling, 
  handleFormChange, 
  formData, 
  total, 
  setLoading, 
  navigate, 
}) => (
  <section aria-labelledby="payment-and-shipping-heading" className="py-16 lg:col-start-1 lg:row-start-1 lg:mx-auto lg:w-full lg:max-w-lg lg:pb-24 lg:pt-0">
    <h2 id="payment-and-shipping-heading" className="sr-only">Payment and shipping details</h2>
    <div>
      <div className="mx-auto max-w-2xl px-4 lg:max-w-none lg:px-0">
        <UserInformation 
          useShippingAsBilling={useShippingAsBilling} 
          setUseShippingAsBilling={handleUseShippingAsBilling} 
          handleFormChange={handleFormChange} 
          formData={formData}
        />
        <Elements stripe={stripePromise}>
          <StripePaymentForm 
            total={parseFloat(total)} 
            formData={formData} 
            setLoading={setLoading} 
            navigate={navigate} 
          />
        </Elements>
      </div>
    </div>
  </section>
);

OrderSummary.propTypes = {
  total: PropTypes.number.isRequired,
  cart: PropTypes.array.isRequired,
};

PaymentAndShipping.propTypes = {
  useShippingAsBilling: PropTypes.bool.isRequired,
  handleUseShippingAsBilling: PropTypes.func.isRequired,
  handleFormChange: PropTypes.func.isRequired,
  formData: PropTypes.object.isRequired,
  total: PropTypes.number.isRequired,
  setLoading: PropTypes.func.isRequired,
  navigate: PropTypes.func.isRequired,
  cart: PropTypes.array.isRequired,
};

export default Checkout;

还有 StripePaymentForm.jsx

import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js';
import axios from 'axios';
import PropTypes from 'prop-types';
import PrimaryButton from '../PrimaryButton';

const StripePaymentForm = ({ total, formData, setLoading, navigate }) => {
  const stripe = useStripe();
  const elements = useElements();

  const secret_key = import.meta.env.VITE_STRIPE_SECRET_KEY;

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

    if (!stripe || !elements) {
      setLoading(false);
      return;
    }

    const cardElement = elements.getElement(CardElement);

    try {
      // Create a PaymentIntent on the client side
      const { data: paymentIntent } = await axios.post(
        'https://api.stripe.com/v1/payment_intents',
        new URLSearchParams({
          amount: Math.round(total * 100),  // Ensure the amount is correctly rounded and converted to cents
          currency: 'usd',
          'payment_method_types[]': 'card',
        }),
        {
          headers: {
            'Authorization': `Bearer ${secret_key}`,
            'Content-Type': 'application/x-www-form-urlencoded',
          }
        }
      );

      const clientSecret = paymentIntent.client_secret;

      const paymentResult = await stripe.confirmCardPayment(clientSecret, {
        payment_method: {
          card: cardElement,
          billing_details: {
            name: `${formData.firstName} ${formData.lastName}`,
            email: formData.email,
            address: {
              line1: formData.address,
              city: formData.city,
              state: formData.state,
              postal_code: formData.zip,
            },
          },
        },
      });

      if (paymentResult.error) {
        console.error('Payment failed:', paymentResult.error);
        setLoading(false);
        return;
      }

      if (paymentResult.paymentIntent.status === 'succeeded') {
        navigate('/confirmation');
      }
    } catch (error) {
      console.error('Error processing payment:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className='grid grid-cols-1 gap-4 items-center mt-4'>
      <div className='col-span-1'>
        <h3 className='text-xl font-bold text-indigo-500'>Payment Information</h3>
        <CardElement className="mt-1 p-2 border border-gray-300 bg-white dark:bg-black rounded" />
      </div>
      <div className='col-span-1'>
        <PrimaryButton
          text={`Pay $${total.toFixed(2)}`}
          onClick={handleSubmit}
          disabled={!stripe}
          className={`w-full ${!stripe ? 'opacity-50 cursor-not-allowed' : ''}`}
        />
      </div>      
    </div>
  );
};

StripePaymentForm.propTypes = {
  total: PropTypes.number.isRequired,
  formData: PropTypes.object.isRequired,
  setLoading: PropTypes.func.isRequired,
  navigate: PropTypes.func.isRequired,
};

export default StripePaymentForm;

非常感谢任何帮助。

enter image description here

javascript reactjs stripe-payments
1个回答
0
投票

Dashboard 上的 PaymentIntent 为“Incomplete”,表示尚未确认。您的代码尚未到达提交功能中的

await stripe.confirmCardPayment

您可以在

handleSubmit
函数中添加更多调试功能。但我也看到您正在从js创建PaymentIntent,您应该永远不要这样做,因为这会将您的密钥泄露给前端。密钥应该隐藏在后端,您应该只调用 AJAX 到后端来检索密钥。

更重要的是,这是 CardElement(您可以在此处下载工作示例),但它是旧版本,实际上您应该改用 PaymentElement

© www.soinside.com 2019 - 2024. All rights reserved.