付款在开发模式下成功完成,但在生产中我收到此错误:v3:1未捕获(承诺中)集成错误:我们无法从指定元素检索数据。请确保您尝试使用的元素仍然已安装。
结账表格:
import { useState } from "react";
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
import axios from "axios";
import { Link, useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import {
selectLoadingState,
startLoading,
stopLoading,
startCheckout,
completeCheckout,
cancelCheckout,
cleanCart,
} from "../redux/user/userSlice";
import PropTypes from "prop-types";
import Loader from "react-loader-spinner";
import { useTranslation } from "react-i18next";
import "../styles/CheckoutForm.css";
const CARD_OPTIONS = {
iconStyle: "solid",
style: {
base: {
iconColor: "#c4f0ff",
color: "#fff",
fontWeight: 500,
fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif",
fontSize: "16px",
fontSmoothing: "antialiased",
":-webkit-autofill": { color: "#fce883" },
"::placeholder": { color: "#87bbfd" },
},
invalid: {
iconColor: "#ffc7ee",
color: "#ffc7ee",
},
},
};
export default function CheckoutForm({ location }) {
const [success, setSuccess] = useState(false);
const dispatch = useDispatch();
const loading = useSelector(selectLoadingState);
const stripe = useStripe();
const elements = useElements();
const navigate = useNavigate();
const { t } = useTranslation();
const handleSubmit = async (e) => {
e.preventDefault();
const { amount, items } = location.state || {};
dispatch(startLoading());
console.log("Total Price:", amount);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: "card",
card: elements.getElement(CardElement),
});
if (!error) {
try {
dispatch(startCheckout());
const { id } = paymentMethod;
const response = await axios.post(
"https://pizzasi.vercel.app/payment",
{
amount: amount,
items: items,
id,
}
);
console.log();
if (response.data.success) {
console.log("Successful payment");
dispatch(stopLoading());
setSuccess(true);
dispatch(completeCheckout({ items, amount }));
dispatch(cleanCart());
navigate("/success");
}
} catch (error) {
console.log("Error", error);
dispatch(stopLoading());
dispatch(cancelCheckout());
navigate("/canceled");
}
} else {
console.log(error.message);
}
};
return (
<div className="container">
{!success && !loading ? (
<form onSubmit={handleSubmit}>
<fieldset className="form-group">
<div className="form-row">
<CardElement options={CARD_OPTIONS} />
</div>
</fieldset>
<button className="payBtn" type="submit">
{loading ? (
<Loader type="Oval" color="#FFF" height={20} width={20} />
) : (
"Pay"
)}
</button>
<Link to="/cart">
<button className="goBacktBtn">{t("goBack")}</button>
</Link>
</form>
) : (
<div>
<h2>{t("paymentSuccess")}</h2>
</div>
)}
</div>
);
}
CheckoutForm.propTypes = {
location: PropTypes.shape({
state: PropTypes.shape({
amount: PropTypes.number.isRequired,
items: PropTypes.array.isRequired,
}).isRequired,
}).isRequired,
};
条纹容器:
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import CheckoutForm from "../pages/CheckoutForm";
import { useSelector } from "react-redux";
import { selectUserCart } from "../redux/user/userSlice";
import { useLocation } from "react-router-dom";
const stripeKey =
import.meta.env.MODE === "development"
? import.meta.env.VITE_STRIPE_KEY_TEST
: import.meta.env.VITE_STRIPE_KEY_LIVE;
const stripeTestPromise = loadStripe(stripeKey);
export default function StripeContainer() {
const location = useLocation();
const cartItems = useSelector(selectUserCart);
const amount = cartItems.reduce(
(total, cartItem) => total + cartItem.price * cartItem.quantity,
0
);
console.log("Total Price stripe:", amount);
return (
<Elements stripe={stripeTestPromise}>
<CheckoutForm amount={amount} location={location} items={cartItems} />
</Elements>
);
}
应用程序:
import { Suspense } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";
import AnimatedRoutes from "./components/AnimatedRoutes";
import Menu from "./pages/Menu";
import About from "./pages/About";
import Contact from "./pages/Contact";
import Cart from "./pages/Cart";
import Login from "./pages/Login";
import CashOnDeliveryForm from "./pages/CashOnDeliveryForm";
import StripeContainer from "./components/StripeContainer";
import NotFoundPage from "./pages/NotFoundPage";
import ProtectedRoute from "./utils/ProtectedRoute";
import SuccessPage from "./pages/SuccessPage";
import CanceledPage from "./pages/CanceledPage";
function App() {
return (
<Suspense fallback={null}>
<Router>
<Navbar />
<Routes>
<Route path="/" element={<AnimatedRoutes />} />
<Route path="/menu" element={<Menu />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}>
<Route exact path="/cart" element={<Cart />} />
<Route path="/stripe-checkout" element={<StripeContainer />} />
<Route path="/cash-on-delivery" element={<CashOnDeliveryForm />} />
</Route>
<Route path="/success" element={<SuccessPage />} />
<Route path="/canceled" element={<CanceledPage />} />
<Route path="/*" replace element={<NotFoundPage />} />
</Routes>
<Footer />
</Router>
</Suspense>
);
}
export default App;
用户切片:
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
user: null,
cart: [],
checkoutStatus: null,
paymentDetails: null,
loading: false,
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
startLoading: (state) => {
state.loading = true;
},
stopLoading: (state) => {
state.loading = false;
},
signIn: (state, action) => {
state.user = action.payload;
},
signOut: (state) => {
state.user = null;
},
updateUserCart: (state, action) => {
state.cart = action.payload.cartItems;
},
addItem: (state, action) => {
state.cart.push(action.payload);
},
removeItem: (state, action) => {
const itemIdToRemove = action.payload;
state.cart = state.cart.filter((item) => item.itemId !== itemIdToRemove);
},
cleanCart: (state) => {
state.cart = [];
},
startCheckout: (state) => {
state.checkoutStatus = "processing";
},
completeCheckout: (state, action) => {
state.checkoutStatus = "completed";
state.paymentDetails = action.payload;
},
cancelCheckout: (state) => {
state.checkoutStatus = "canceled";
},
},
});
export const {
startLoading,
stopLoading,
signIn,
signOut: performingSignOut,
updateUserCart,
addItem,
removeItem,
cleanCart,
startCheckout,
completeCheckout,
cancelCheckout,
} = userSlice.actions;
export const selectLoadingState = (state) => state.user.loading;
export const selectUser = (state) => state.user.user;
export const selectUserCart = (state) => state.user.cart;
export const selectCheckoutStatus = (state) => state.user?.checkoutStatus;
export const selectPaymentDetails = (state) => state.user?.paymentDetails;
export default userSlice.reducer;
服务器:
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
require("dotenv").config();
const cors = require("cors");
const { body, validationResult } = require("express-validator");
const admin = require("firebase-admin");
const serviceAccount = require("./serviceAccountKey.js");
const stripeSecretKey =
process.env.NODE_ENV === "development"
? process.env.STRIPE_SECRET_KEY_TEST
: process.env.STRIPE_SECRET_KEY_LIVE;
const stripe = require("stripe")(stripeSecretKey);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.DB_URL,
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
const corsOptions = {
origin: ["https://pizzasi.vercel.app/"],
methods: "POST",
};
app.post("/payment", cors(corsOptions), async (req, res) => {
console.log(req.body);
let { amount, id, items } = req.body;
try {
if (isNaN(amount)) {
console.error("Invalid amount:", amount);
throw new Error("Invalid amount");
}
const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100,
currency: "EUR",
description: "PizzaSi company",
payment_method_types: ["card", "ideal"],
payment_method: id,
confirm: true,
return_url: "https://pizzasi.vercel.app/success",
});
console.log("Payment", paymentIntent);
res.status(200).json({
message: "Payment successful",
success: true,
loading: false,
});
} catch (error) {
console.log("Error", error);
res.status(500).json({
message: "Payment failed",
success: false,
loading: false,
});
}
});
app.post("/create-order", cors(), async (req, res) => {
const orderDetails = req.body;
[
body("formData.name").isString(),
body("formData.address").isString(),
body("formData.phone").custom((value) => {
const normalizePhone = value.replace(/[^0-9]/g, "");
const phoneRegex = /^(\+)?(216)?(\d{8})£/;
if (!phoneRegex.test(normalizePhone)) {
throw new Error("Please enter a valid phone number");
}
return true;
}),
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
};
try {
const db = admin.database();
const ordersRef = db.ref("orders");
const newOrderRef = ordersRef.push();
await newOrderRef.set(orderDetails);
res.status(200).json({
message: "Order created successfully",
success: true,
loading: false,
});
} catch (error) {
console.log("Error", error);
res.status(500).json({
message: "Failed to create order",
success: false,
loading: false,
});
}
});
app.listen(3001, () => {
console.log("Server is running on port 3001");
});
这几乎肯定是由于您的
loading
状态导致,一旦您点击“支付”并拨打 CardElement
,handleSubmit
就会卸载。您需要确保 CardElement
保持安装状态,以便 Stripe.js 访问客户卡详细信息。
{!success && !loading ? (
...
<CardElement options={CARD_OPTIONS} />
...
) : (
<div>
<h2>{t("paymentSuccess")}</h2>
</div>
)}
一旦
loading
设置为 true,卡片元素就会从此处的 DOM 中删除,这将导致像您遇到的错误。