From e23ec2ca403115f688f73010a03d307410564d2d Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Thu, 26 Mar 2026 21:26:38 +0100 Subject: [PATCH] feat: add payment form with mollie components card fields --- components/checkout/PaymentForm.tsx | 256 ++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 components/checkout/PaymentForm.tsx diff --git a/components/checkout/PaymentForm.tsx b/components/checkout/PaymentForm.tsx new file mode 100644 index 0000000..a9f5304 --- /dev/null +++ b/components/checkout/PaymentForm.tsx @@ -0,0 +1,256 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import Script from 'next/script' +import { Lock, ShieldCheck } from '@phosphor-icons/react' +import { COUNTRY_OPTIONS } from '@/lib/countries' +import { initMollie, getMollie, MOLLIE_FIELD_STYLES, type MollieComponent } from '@/lib/mollie' +import { createEmbeddedCheckout } from '@/lib/api/billing' + +interface PaymentFormProps { + plan: string + interval: string + limit: number +} + +const inputClass = + 'w-full rounded-lg border border-neutral-700 bg-neutral-800/50 px-3 py-2.5 text-sm text-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-brand-orange focus:border-brand-orange transition-colors' +const mollieFieldClass = + 'w-full rounded-lg border border-neutral-700 bg-neutral-800/50 px-3 py-2.5 h-[42px] transition-colors focus-within:ring-1 focus-within:ring-brand-orange focus-within:border-brand-orange' + +export default function PaymentForm({ plan, interval, limit }: PaymentFormProps) { + const router = useRouter() + const searchParams = useSearchParams() + const currentInterval = searchParams.get('interval') || interval + + const [country, setCountry] = useState('') + const [vatId, setVatId] = useState('') + const [mollieReady, setMollieReady] = useState(false) + const [mollieError, setMollieError] = useState(false) + const [formError, setFormError] = useState(null) + const [cardErrors, setCardErrors] = useState>({}) + const [submitting, setSubmitting] = useState(false) + + const componentsRef = useRef>({ + cardNumber: null, + cardExpiry: null, + cardCvc: null, + }) + + const mountMollieComponents = () => { + const mollie = initMollie() + if (!mollie) { + setMollieError(true) + return + } + + try { + const fields: Array<{ type: string; selector: string }> = [ + { type: 'cardNumber', selector: '#mollie-card-number' }, + { type: 'cardExpiry', selector: '#mollie-card-expiry' }, + { type: 'cardCvc', selector: '#mollie-card-cvc' }, + ] + + for (const { type, selector } of fields) { + const component = mollie.createComponent(type, { styles: MOLLIE_FIELD_STYLES }) + component.mount(selector) + component.addEventListener('change', (event: unknown) => { + const e = event as { error?: string; touched?: boolean } + setCardErrors((prev) => { + const next = { ...prev } + if (e.error) next[type] = e.error + else delete next[type] + return next + }) + }) + componentsRef.current[type] = component + } + + setMollieReady(true) + } catch { + setMollieError(true) + } + } + + // Cleanup Mollie components on unmount + useEffect(() => { + return () => { + Object.values(componentsRef.current).forEach((c) => c?.unmount()) + } + }, []) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setFormError(null) + if (!country) { + setFormError('Please select your country') + return + } + + const mollie = getMollie() + if (!mollie) { + setFormError('Payment system not loaded. Please refresh.') + return + } + + setSubmitting(true) + try { + const { token, error } = await mollie.createToken() + if (error || !token) { + setFormError(error?.message || 'Invalid card details.') + setSubmitting(false) + return + } + + const result = await createEmbeddedCheckout({ + plan_id: plan, + interval: currentInterval, + limit, + country, + vat_id: vatId || undefined, + card_token: token, + }) + + if (result.status === 'success') router.push('/checkout?status=success') + else if (result.status === 'pending' && result.redirect_url) + window.location.href = result.redirect_url + } catch (err) { + setFormError((err as Error)?.message || 'Payment failed. Please try again.') + } finally { + setSubmitting(false) + } + } + + return ( + <> +