Add Mollie payment integration: Implement lazy loading for PaymentSuccess page, update CartTab and OrderProcessingService to handle Mollie payment intents, and enhance ProfilePage to manage payment completion for both Stripe and Mollie. Update base URL for development environment.
This commit is contained in:
@@ -55,6 +55,9 @@ const ChatAssistant = lazy(() => import(/* webpackChunkName: "chat" */ "./compon
|
||||
const PresseverleihPage = lazy(() => import(/* webpackChunkName: "presseverleih" */ "./pages/PresseverleihPage.js"));
|
||||
const ThcTestPage = lazy(() => import(/* webpackChunkName: "thc-test" */ "./pages/ThcTestPage.js"));
|
||||
|
||||
// Lazy load payment success page
|
||||
const PaymentSuccess = lazy(() => import(/* webpackChunkName: "payment" */ "./components/PaymentSuccess.js"));
|
||||
|
||||
// Import theme from separate file to reduce main bundle size
|
||||
import defaultTheme from "./theme.js";
|
||||
// Lazy load theme customizer for development only
|
||||
@@ -224,6 +227,9 @@ const AppContent = ({ currentTheme, onThemeChange }) => {
|
||||
{/* Profile page */}
|
||||
<Route path="/profile" element={<ProfilePageWithSocket />} />
|
||||
|
||||
{/* Payment success page for Mollie redirects */}
|
||||
<Route path="/payment/success" element={<PaymentSuccess />} />
|
||||
|
||||
{/* Reset password page */}
|
||||
<Route
|
||||
path="/resetPassword"
|
||||
|
||||
189
src/components/PaymentSuccess.js
Normal file
189
src/components/PaymentSuccess.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
import SocketContext from '../contexts/SocketContext.js';
|
||||
|
||||
class PaymentSuccess extends Component {
|
||||
static contextType = SocketContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
redirectUrl: null,
|
||||
processing: true,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.processMolliePayment();
|
||||
}
|
||||
|
||||
processMolliePayment = () => {
|
||||
try {
|
||||
// Get the stored payment ID from localStorage
|
||||
const pendingPayment = localStorage.getItem('pendingPayment');
|
||||
|
||||
if (!pendingPayment) {
|
||||
console.error('No pending payment found in localStorage');
|
||||
this.setState({
|
||||
redirectUrl: '/profile#cart',
|
||||
processing: false,
|
||||
error: 'No payment information found'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let paymentData;
|
||||
try {
|
||||
paymentData = JSON.parse(pendingPayment);
|
||||
// Clear the pending payment data
|
||||
localStorage.removeItem('pendingPayment');
|
||||
} catch (error) {
|
||||
console.error('Error parsing pending payment data:', error);
|
||||
this.setState({
|
||||
redirectUrl: '/profile#cart',
|
||||
processing: false,
|
||||
error: 'Invalid payment data'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!paymentData.paymentId) {
|
||||
console.error('No payment ID found in stored payment data');
|
||||
this.setState({
|
||||
redirectUrl: '/profile#cart',
|
||||
processing: false,
|
||||
error: 'Missing payment ID'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check payment status with backend
|
||||
this.checkMolliePaymentStatus(paymentData.paymentId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing Mollie payment:', error);
|
||||
this.setState({
|
||||
redirectUrl: '/profile#cart',
|
||||
processing: false,
|
||||
error: 'Payment processing failed'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
checkMolliePaymentStatus = (paymentId) => {
|
||||
const { socket } = this.context;
|
||||
|
||||
if (!socket || !socket.connected) {
|
||||
console.error('Socket not connected');
|
||||
this.setState({
|
||||
redirectUrl: '/profile#cart',
|
||||
processing: false,
|
||||
error: 'Connection error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('checkMollieIntent', { paymentId }, (response) => {
|
||||
if (response.success) {
|
||||
console.log('Payment Status:', response.payment.status);
|
||||
console.log('Is Paid:', response.payment.isPaid);
|
||||
console.log('Order Created:', response.order.created);
|
||||
|
||||
if (response.order.orderId) {
|
||||
console.log('Order ID:', response.order.orderId);
|
||||
}
|
||||
|
||||
// Build the redirect URL with Mollie completion parameters
|
||||
const profileUrl = new URL('/profile', window.location.origin);
|
||||
profileUrl.searchParams.set('mollieComplete', 'true');
|
||||
profileUrl.searchParams.set('mollie_payment_id', paymentId);
|
||||
profileUrl.searchParams.set('mollie_status', response.payment.status);
|
||||
profileUrl.searchParams.set('mollie_amount', response.payment.amount);
|
||||
profileUrl.searchParams.set('mollie_timestamp', Date.now().toString());
|
||||
profileUrl.searchParams.set('mollie_is_paid', response.payment.isPaid.toString());
|
||||
|
||||
if (response.order.orderId) {
|
||||
profileUrl.searchParams.set('mollie_order_id', response.order.orderId.toString());
|
||||
}
|
||||
|
||||
// Set hash to cart tab
|
||||
profileUrl.hash = '#cart';
|
||||
|
||||
this.setState({
|
||||
redirectUrl: profileUrl.pathname + profileUrl.search + profileUrl.hash,
|
||||
processing: false
|
||||
});
|
||||
} else {
|
||||
console.error('Failed to check payment status:', response.error);
|
||||
this.setState({
|
||||
redirectUrl: '/profile#cart',
|
||||
processing: false,
|
||||
error: response.error || 'Payment verification failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { redirectUrl, processing, error } = this.state;
|
||||
|
||||
if (processing) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '60vh',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={60} />
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Zahlung wird überprüft...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Bitte warten Sie, während wir Ihre Zahlung bei Mollie überprüfen.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '60vh',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="error">
|
||||
Zahlungsüberprüfung fehlgeschlagen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{error}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Sie werden zu Ihrem Profil weitergeleitet...
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (redirectUrl) {
|
||||
return <Navigate to={redirectUrl} replace />;
|
||||
}
|
||||
|
||||
// Fallback redirect to profile
|
||||
return <Navigate to="/profile#cart" replace />;
|
||||
}
|
||||
}
|
||||
|
||||
export default PaymentSuccess;
|
||||
@@ -377,34 +377,23 @@ class CartTab extends Component {
|
||||
(total, item) => total + item.price * item.quantity,
|
||||
0
|
||||
);
|
||||
const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents
|
||||
const totalAmount = Math.round((subtotal + deliveryCost) * 100) / 100;
|
||||
|
||||
if (this.context && this.context.socket && this.context.socket.connected) {
|
||||
this.context.socket.emit(
|
||||
"createMollieIntent",
|
||||
{ amount: totalAmount, invoiceId: 'A-34344' },
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
|
||||
localStorage.setItem('pendingPayment', JSON.stringify({
|
||||
paymentId: response.paymentId,
|
||||
amount: totalAmount,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
window.location.href = response.checkoutUrl;
|
||||
|
||||
} else {
|
||||
console.error("Error:", response.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.error("Socket context not available");
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: "Cannot connect to server. Please try again later.",
|
||||
});
|
||||
}
|
||||
// Prepare complete order data for Mollie intent creation
|
||||
const mollieOrderData = {
|
||||
amount: totalAmount,
|
||||
items: cartItems,
|
||||
invoiceAddress,
|
||||
deliveryAddress: useSameAddress ? invoiceAddress : deliveryAddress,
|
||||
deliveryMethod,
|
||||
paymentMethod: "mollie",
|
||||
deliveryCost,
|
||||
note,
|
||||
domain: window.location.origin,
|
||||
saveAddressForFuture,
|
||||
};
|
||||
|
||||
this.orderService.createMollieIntent(mollieOrderData);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,18 +49,29 @@ class OrderProcessingService {
|
||||
waitForVerifyTokenAndProcessOrder() {
|
||||
// Check if window.cart is already populated (verifyToken already completed)
|
||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||
this.processStripeOrderWithCart(window.cart);
|
||||
if (this.paymentCompletionData && this.paymentCompletionData.paymentType === 'mollie') {
|
||||
this.processMollieOrderWithCart(window.cart);
|
||||
} else {
|
||||
this.processStripeOrderWithCart(window.cart);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for cart event which is dispatched after verifyToken completes
|
||||
this.verifyTokenHandler = () => {
|
||||
if (Array.isArray(window.cart) && window.cart.length > 0) {
|
||||
this.processStripeOrderWithCart([...window.cart]); // Copy the cart
|
||||
const cartCopy = [...window.cart]; // Copy the cart
|
||||
|
||||
// Clear window.cart after copying
|
||||
window.cart = [];
|
||||
window.dispatchEvent(new CustomEvent("cart"));
|
||||
|
||||
// Process based on payment type
|
||||
if (this.paymentCompletionData && this.paymentCompletionData.paymentType === 'mollie') {
|
||||
this.processMollieOrderWithCart(cartCopy);
|
||||
} else {
|
||||
this.processStripeOrderWithCart(cartCopy);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
completionError: "Cart is empty. Please add items to your cart before placing an order."
|
||||
@@ -111,6 +122,21 @@ class OrderProcessingService {
|
||||
});
|
||||
}
|
||||
|
||||
processMollieOrderWithCart(cartItems) {
|
||||
// Clear timeout if it exists
|
||||
if (this.verifyTokenTimeout) {
|
||||
clearTimeout(this.verifyTokenTimeout);
|
||||
this.verifyTokenTimeout = null;
|
||||
}
|
||||
|
||||
// Store cart items in state and process order
|
||||
this.setState({
|
||||
originalCartItems: cartItems
|
||||
}, () => {
|
||||
this.processMollieOrder();
|
||||
});
|
||||
}
|
||||
|
||||
processStripeOrder() {
|
||||
// If no original cart items, don't process
|
||||
if (!this.getState().originalCartItems || this.getState().originalCartItems.length === 0) {
|
||||
@@ -205,6 +231,20 @@ class OrderProcessingService {
|
||||
});
|
||||
}
|
||||
|
||||
processMollieOrder() {
|
||||
// For Mollie payments, the backend handles order creation automatically
|
||||
// when payment is successful. We just need to show success state.
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
orderCompleted: true,
|
||||
completionError: null,
|
||||
});
|
||||
|
||||
// Clear the cart since order was created by backend
|
||||
window.cart = [];
|
||||
window.dispatchEvent(new CustomEvent("cart"));
|
||||
}
|
||||
|
||||
// Process regular (non-Stripe) orders
|
||||
processRegularOrder(orderData) {
|
||||
const context = this.getContext();
|
||||
@@ -271,6 +311,40 @@ class OrderProcessingService {
|
||||
}
|
||||
}
|
||||
|
||||
// Create Mollie payment intent
|
||||
createMollieIntent(mollieOrderData) {
|
||||
const context = this.getContext();
|
||||
if (context && context.socket && context.socket.connected) {
|
||||
context.socket.emit(
|
||||
"createMollieIntent",
|
||||
mollieOrderData,
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
// Store pending payment info and redirect
|
||||
localStorage.setItem('pendingPayment', JSON.stringify({
|
||||
paymentId: response.paymentId,
|
||||
amount: mollieOrderData.amount,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
window.location.href = response.checkoutUrl;
|
||||
} else {
|
||||
console.error("Error:", response.error);
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: response.error || "Failed to create Mollie payment intent. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.error("Socket context not available");
|
||||
this.setState({
|
||||
isCompletingOrder: false,
|
||||
completionError: "Cannot connect to server. Please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate delivery cost
|
||||
getDeliveryCost() {
|
||||
const { deliveryMethod, paymentMethod } = this.getState();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const config = {
|
||||
baseUrl: "https://growheads.de",
|
||||
baseUrl: "https://dev.seedheads.de",
|
||||
apiBaseUrl: "",
|
||||
googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com",
|
||||
stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu",
|
||||
|
||||
@@ -28,9 +28,11 @@ const ProfilePage = (props) => {
|
||||
const [orderIdFromHash, setOrderIdFromHash] = useState(null);
|
||||
const [paymentCompletion, setPaymentCompletion] = useState(null);
|
||||
|
||||
// @note Check for payment completion parameters from Stripe redirect
|
||||
// @note Check for payment completion parameters from Stripe and Mollie redirects
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
// Check for Stripe payment completion
|
||||
const isComplete = urlParams.has('complete');
|
||||
const paymentIntent = urlParams.get('payment_intent');
|
||||
const paymentIntentClientSecret = urlParams.get('payment_intent_client_secret');
|
||||
@@ -38,6 +40,7 @@ const ProfilePage = (props) => {
|
||||
|
||||
if (isComplete && paymentIntent && redirectStatus) {
|
||||
setPaymentCompletion({
|
||||
paymentType: 'stripe',
|
||||
paymentIntent,
|
||||
paymentIntentClientSecret,
|
||||
redirectStatus,
|
||||
@@ -52,6 +55,39 @@ const ProfilePage = (props) => {
|
||||
newUrl.searchParams.delete('redirect_status');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
|
||||
// Check for Mollie payment completion
|
||||
const isMollieComplete = urlParams.has('mollieComplete');
|
||||
const molliePaymentId = urlParams.get('mollie_payment_id');
|
||||
const mollieStatus = urlParams.get('mollie_status');
|
||||
const mollieAmount = urlParams.get('mollie_amount');
|
||||
const mollieTimestamp = urlParams.get('mollie_timestamp');
|
||||
const mollieIsPaid = urlParams.get('mollie_is_paid');
|
||||
const mollieOrderId = urlParams.get('mollie_order_id');
|
||||
|
||||
if (isMollieComplete && molliePaymentId && mollieStatus) {
|
||||
setPaymentCompletion({
|
||||
paymentType: 'mollie',
|
||||
molliePaymentId,
|
||||
mollieStatus,
|
||||
mollieAmount: mollieAmount ? parseFloat(mollieAmount) : null,
|
||||
mollieTimestamp: mollieTimestamp ? parseInt(mollieTimestamp) : null,
|
||||
mollieIsPaid: mollieIsPaid === 'true',
|
||||
mollieOrderId: mollieOrderId ? parseInt(mollieOrderId) : null,
|
||||
isSuccessful: mollieIsPaid === 'true'
|
||||
});
|
||||
|
||||
// Clean up the URL by removing the Mollie payment parameters
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.delete('mollieComplete');
|
||||
newUrl.searchParams.delete('mollie_payment_id');
|
||||
newUrl.searchParams.delete('mollie_status');
|
||||
newUrl.searchParams.delete('mollie_amount');
|
||||
newUrl.searchParams.delete('mollie_timestamp');
|
||||
newUrl.searchParams.delete('mollie_is_paid');
|
||||
newUrl.searchParams.delete('mollie_order_id');
|
||||
window.history.replaceState({}, '', newUrl.toString());
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user