From ccceb8fe783f2a470a21a3f271e4a57fa86f6b2c Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 6 Jul 2025 02:21:52 +0200 Subject: [PATCH] Add Mollie payment integration to CartTab and OrderProcessingService - Introduced Mollie payment method in CartTab, allowing users to select it alongside existing payment options. - Implemented loading and error handling for the Mollie component. - Updated OrderProcessingService to create Mollie payment intents. - Adjusted PaymentMethodSelector to switch to Mollie when specific delivery methods are selected. - Enhanced CartTab to store cart items for Mollie payments and calculate total amounts accordingly. --- src/components/Mollie.js | 381 ++++++++++++++++++ src/components/profile/CartTab.js | 76 +++- .../profile/OrderProcessingService.js | 4 + .../profile/PaymentMethodSelector.js | 18 +- src/config.js | 1 + 5 files changed, 473 insertions(+), 7 deletions(-) create mode 100644 src/components/Mollie.js diff --git a/src/components/Mollie.js b/src/components/Mollie.js new file mode 100644 index 0000000..e35de51 --- /dev/null +++ b/src/components/Mollie.js @@ -0,0 +1,381 @@ +import React, { Component, useState } from "react"; +import { Button, Box, Typography, CircularProgress } from "@mui/material"; +import config from "../config.js"; + +// Function to lazy load Mollie script +const loadMollie = () => { + return new Promise((resolve, reject) => { + // Check if Mollie is already loaded + if (window.Mollie) { + resolve(window.Mollie); + return; + } + + // Create script element + const script = document.createElement('script'); + script.src = 'https://js.mollie.com/v1/mollie.js'; + script.async = true; + + script.onload = () => { + if (window.Mollie) { + resolve(window.Mollie); + } else { + reject(new Error('Mollie failed to load')); + } + }; + + script.onerror = () => { + reject(new Error('Failed to load Mollie script')); + }; + + document.head.appendChild(script); + }); +}; + +const CheckoutForm = ({ mollie }) => { + const [errorMessage, setErrorMessage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + React.useEffect(() => { + if (!mollie) return; + + let mountedComponents = { + cardNumber: null, + cardHolder: null, + expiryDate: null, + verificationCode: null + }; + + try { + // Create Mollie components + const cardNumber = mollie.createComponent('cardNumber'); + const cardHolder = mollie.createComponent('cardHolder'); + const expiryDate = mollie.createComponent('expiryDate'); + const verificationCode = mollie.createComponent('verificationCode'); + + // Store references for cleanup + mountedComponents = { + cardNumber, + cardHolder, + expiryDate, + verificationCode + }; + + // Mount components + cardNumber.mount('#card-number'); + cardHolder.mount('#card-holder'); + expiryDate.mount('#expiry-date'); + verificationCode.mount('#verification-code'); + + // Set up error handling + cardNumber.addEventListener('change', event => { + const errorElement = document.querySelector('#card-number-error'); + if (errorElement) { + if (event.error && event.touched) { + errorElement.textContent = event.error; + } else { + errorElement.textContent = ''; + } + } + }); + + cardHolder.addEventListener('change', event => { + const errorElement = document.querySelector('#card-holder-error'); + if (errorElement) { + if (event.error && event.touched) { + errorElement.textContent = event.error; + } else { + errorElement.textContent = ''; + } + } + }); + + expiryDate.addEventListener('change', event => { + const errorElement = document.querySelector('#expiry-date-error'); + if (errorElement) { + if (event.error && event.touched) { + errorElement.textContent = event.error; + } else { + errorElement.textContent = ''; + } + } + }); + + verificationCode.addEventListener('change', event => { + const errorElement = document.querySelector('#verification-code-error'); + if (errorElement) { + if (event.error && event.touched) { + errorElement.textContent = event.error; + } else { + errorElement.textContent = ''; + } + } + }); + + // Components are now mounted and ready + + } catch (error) { + console.error('Error creating Mollie components:', error); + setErrorMessage('Failed to initialize payment form. Please try again.'); + } + + // Cleanup function + return () => { + try { + if (mountedComponents.cardNumber) mountedComponents.cardNumber.unmount(); + if (mountedComponents.cardHolder) mountedComponents.cardHolder.unmount(); + if (mountedComponents.expiryDate) mountedComponents.expiryDate.unmount(); + if (mountedComponents.verificationCode) mountedComponents.verificationCode.unmount(); + } catch (error) { + console.error('Error cleaning up Mollie components:', error); + } + }; + }, [mollie]); + + const handleSubmit = async (event) => { + event.preventDefault(); + + if (!mollie || isSubmitting) { + return; + } + + setIsSubmitting(true); + setErrorMessage(null); + + try { + const { token, error } = await mollie.createToken(); + + if (error) { + setErrorMessage(error.message || 'Payment failed. Please try again.'); + setIsSubmitting(false); + return; + } + + if (token) { + // Handle successful token creation + // Create a payment completion event similar to Stripe + const mollieCompletionData = { + mollieToken: token, + paymentMethod: 'mollie' + }; + + // Dispatch a custom event to notify the parent component + const completionEvent = new CustomEvent('molliePaymentComplete', { + detail: mollieCompletionData + }); + window.dispatchEvent(completionEvent); + + // For now, redirect to profile with completion data + const returnUrl = `${window.location.origin}/profile?complete&mollie_token=${token}`; + window.location.href = returnUrl; + } + } catch (error) { + console.error('Error creating Mollie token:', error); + setErrorMessage('Payment failed. Please try again.'); + setIsSubmitting(false); + } + }; + + return ( + + + Kreditkarte oder Sofortüberweisung + + +
+ + + Kartennummer + + + + + + + + Karteninhaber + + + + + + + + + Ablaufdatum + + + + + + + + Sicherheitscode + + + + + + + + + {errorMessage && ( + + {errorMessage} + + )} + + + ); +}; + +class Mollie extends Component { + constructor(props) { + super(props); + this.state = { + mollie: null, + loading: true, + error: null, + }; + this.molliePromise = loadMollie(); + } + + componentDidMount() { + this.molliePromise + .then((MollieClass) => { + try { + // Initialize Mollie with profile key + const mollie = MollieClass(config.mollieProfileKey, { + locale: 'de_DE', + testmode: true // Set to false for production + }); + this.setState({ mollie, loading: false }); + } catch (error) { + console.error('Error initializing Mollie:', error); + this.setState({ + error: 'Failed to initialize payment system. Please try again.', + loading: false + }); + } + }) + .catch((error) => { + console.error('Error loading Mollie:', error); + this.setState({ + error: 'Failed to load payment system. Please try again.', + loading: false + }); + }); + } + + render() { + const { mollie, loading, error } = this.state; + + if (loading) { + return ( + + + + Zahlungskomponente wird geladen... + + + ); + } + + if (error) { + return ( + + + {error} + + + + ); + } + + return ; + } +} + +export default Mollie; diff --git a/src/components/profile/CartTab.js b/src/components/profile/CartTab.js index 4cc6f0e..a579a58 100644 --- a/src/components/profile/CartTab.js +++ b/src/components/profile/CartTab.js @@ -51,6 +51,9 @@ class CartTab extends Component { showStripePayment: false, StripeComponent: null, isLoadingStripe: false, + showMolliePayment: false, + MollieComponent: null, + isLoadingMollie: false, showPaymentConfirmation: false, orderCompleted: false, originalCartItems: [] @@ -116,7 +119,7 @@ class CartTab extends Component { // Determine payment method - respect constraints let prefillPaymentMethod = template.payment_method || "wire"; const paymentMethodMap = { - "credit_card": "stripe", + "credit_card": "mollie",//stripe "bank_transfer": "wire", "cash_on_delivery": "onDelivery", "cash": "cash" @@ -319,6 +322,27 @@ class CartTab extends Component { } }; + loadMollieComponent = async () => { + this.setState({ isLoadingMollie: true }); + + try { + const { default: Mollie } = await import("../Mollie.js"); + this.setState({ + MollieComponent: Mollie, + showMolliePayment: true, + isCompletingOrder: false, + isLoadingMollie: false, + }); + } catch (error) { + console.error("Failed to load Mollie component:", error); + this.setState({ + isCompletingOrder: false, + isLoadingMollie: false, + completionError: "Failed to load payment component. Please try again.", + }); + } + }; + handleCompleteOrder = () => { this.setState({ completionError: null }); // Clear previous errors @@ -363,6 +387,25 @@ class CartTab extends Component { this.orderService.createStripeIntent(totalAmount, this.loadStripeComponent); return; } + // Handle Mollie payment differently + if (paymentMethod === "mollie") { + // Store the cart items used for Mollie payment in sessionStorage for later reference + try { + sessionStorage.setItem('molliePaymentCart', JSON.stringify(cartItems)); + } catch (error) { + console.error("Failed to store Mollie payment cart:", error); + } + + // Calculate total amount for Mollie + const subtotal = cartItems.reduce( + (total, item) => total + item.price * item.quantity, + 0 + ); + const totalAmount = Math.round((subtotal + deliveryCost) * 100); // Convert to cents + + this.orderService.createMollieIntent(totalAmount, this.loadMollieComponent); + return; + } // Handle regular orders const orderData = { @@ -398,6 +441,9 @@ class CartTab extends Component { showStripePayment, StripeComponent, isLoadingStripe, + showMolliePayment, + MollieComponent, + isLoadingMollie, showPaymentConfirmation, orderCompleted, } = this.state; @@ -434,7 +480,7 @@ class CartTab extends Component { @@ -442,7 +488,7 @@ class CartTab extends Component { {cartItems.length > 0 && ( - {isLoadingStripe ? ( + {isLoadingStripe || isLoadingMollie ? ( Zahlungskomponente wird geladen... @@ -468,9 +514,29 @@ class CartTab extends Component { + ) : showMolliePayment && MollieComponent ? ( + <> + + + + + ) : ( { if ((deliveryMethod === "DHL" || deliveryMethod === "DPD") && paymentMethod === "cash") { - handlePaymentMethodChange({ target: { value: "stripe" } }); + handlePaymentMethodChange({ target: { value: "mollie" } }); } }, [deliveryMethod, paymentMethod, handlePaymentMethodChange]); @@ -42,8 +42,22 @@ const PaymentMethodSelector = ({ paymentMethod, onChange, deliveryMethod, onDeli description: "Bezahlen Sie per Banküberweisung", disabled: totalAmount === 0, }, - { + /*{ id: "stripe", + name: "Karte oder Sofortüberweisung (Stripe)", + description: totalAmount < 0.50 && totalAmount > 0 + ? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)" + : "Bezahlen Sie per Karte oder Sofortüberweisung", + disabled: totalAmount < 0.50 || (deliveryMethod !== "DHL" && deliveryMethod !== "DPD" && deliveryMethod !== "Abholung"), + icons: [ + "/assets/images/giropay.png", + "/assets/images/maestro.png", + "/assets/images/mastercard.png", + "/assets/images/visa_electron.png", + ], + },*/ + { + id: "mollie", name: "Karte oder Sofortüberweisung", description: totalAmount < 0.50 && totalAmount > 0 ? "Bezahlen Sie per Karte oder Sofortüberweisung (Mindestbetrag: 0,50 €)" diff --git a/src/config.js b/src/config.js index eae36db..99c3682 100644 --- a/src/config.js +++ b/src/config.js @@ -3,6 +3,7 @@ const config = { apiBaseUrl: "", googleClientId: "928121624463-jbgfdlgem22scs1k9c87ucg4ffvaik6o.apps.googleusercontent.com", stripePublishableKey: "pk_test_51R7lltRtpe3h1vwJzIrDb5bcEigTLBHrtqj9SiPX7FOEATSuD6oJmKc8xpNp49ShpGJZb2GShHIUqj4zlSIz4olj00ipOuOAnu", + mollieProfileKey: "pfl_AtcRTimCff", // SEO and Business Information siteName: "Growheads.de",