Files
personal_website/src/PayPalPayment.tsx
T
jory b42ff297eb Fix Tetris 2P preview block synchronization issue
- Prevent duplicate piece shapes when generating next pieces
- Enhanced randomTetromino() with excludeShape parameter
- Update piece placement logic to avoid visual duplicates
- Improve initial state generation for both players
- Add P1 and P2 next piece previews in 2P mode
2026-01-06 21:54:11 +01:00

311 lines
9.4 KiB
TypeScript

import { useEffect, useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { Loader2, CreditCard, Check } from "lucide-react";
// PayPal SDK types
declare global {
interface Window {
paypal?: any;
}
}
interface PayPalPaymentProps {
amount: string;
currency?: string;
item: string;
onPaymentComplete?: (orderId: string) => void;
}
type PaymentMethod = 'paypal' | 'card' | 'ideal';
const PayPalPayment = ({
amount,
currency = "EUR",
item,
onPaymentComplete
}: PayPalPaymentProps) => {
const [sdkReady, setSdkReady] = useState(false);
const [loading, setLoading] = useState(true);
const [selectedMethod, setSelectedMethod] = useState<PaymentMethod>('paypal');
const [paymentComplete, setPaymentComplete] = useState(false);
const [orderId, setOrderId] = useState<string | null>(null);
const paypalButtonRef = useRef<HTMLDivElement>(null);
const cardButtonRef = useRef<HTMLDivElement>(null);
const idealButtonRef = useRef<HTMLDivElement>(null);
const { toast } = useToast();
// Replace with your PayPal Client ID
// Get one at: https://developer.paypal.com/dashboard/applications
const PAYPAL_CLIENT_ID = "YOUR_PAYPAL_CLIENT_ID";
useEffect(() => {
const loadPayPalSDK = () => {
if (window.paypal) {
setSdkReady(true);
setLoading(false);
return;
}
const script = document.createElement("script");
script.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}&currency=${currency}&components=buttons,funding-eligibility&enable-funding=ideal,card`;
script.async = true;
script.onload = () => {
setSdkReady(true);
setLoading(false);
};
script.onerror = () => {
console.error("PayPal SDK failed to load");
setLoading(false);
toast({
title: "Error",
description: "Failed to load payment system. Check your Client ID.",
variant: "destructive",
});
};
document.body.appendChild(script);
};
loadPayPalSDK();
return () => {
// Cleanup if needed
};
}, [currency, toast]);
useEffect(() => {
if (!sdkReady || !window.paypal || paymentComplete) return;
const createOrder = (_data: any, actions: any) => {
return actions.order.create({
purchase_units: [{
description: item,
amount: {
currency_code: currency,
value: amount,
},
}],
});
};
const onApprove = async (_data: any, actions: any) => {
try {
const order = await actions.order.capture();
console.log("Payment successful:", order);
setPaymentComplete(true);
setOrderId(order.id);
toast({
title: "Payment Successful!",
description: `Order ${order.id} has been confirmed.`,
});
onPaymentComplete?.(order.id);
} catch (error) {
console.error("Payment capture error:", error);
toast({
title: "Payment Failed",
description: "There was an error processing your payment.",
variant: "destructive",
});
}
};
const onError = (err: any) => {
console.error("PayPal error:", err);
toast({
title: "Payment Error",
description: "Something went wrong. Please try again.",
variant: "destructive",
});
};
// Render PayPal button
if (paypalButtonRef.current && selectedMethod === 'paypal') {
paypalButtonRef.current.innerHTML = '';
window.paypal.Buttons({
fundingSource: window.paypal.FUNDING.PAYPAL,
style: {
layout: 'vertical',
color: 'black',
shape: 'rect',
label: 'paypal',
height: 40,
},
createOrder,
onApprove,
onError,
}).render(paypalButtonRef.current);
}
// Render Card button
if (cardButtonRef.current && selectedMethod === 'card') {
cardButtonRef.current.innerHTML = '';
window.paypal.Buttons({
fundingSource: window.paypal.FUNDING.CARD,
style: {
layout: 'vertical',
color: 'black',
shape: 'rect',
height: 40,
},
createOrder,
onApprove,
onError,
}).render(cardButtonRef.current);
}
// Render iDEAL button (available in Netherlands)
if (idealButtonRef.current && selectedMethod === 'ideal') {
idealButtonRef.current.innerHTML = '';
if (window.paypal.FUNDING.IDEAL) {
window.paypal.Buttons({
fundingSource: window.paypal.FUNDING.IDEAL,
style: {
layout: 'vertical',
shape: 'rect',
height: 40,
},
createOrder,
onApprove,
onError,
}).render(idealButtonRef.current);
} else {
idealButtonRef.current.innerHTML = '<p class="text-xs text-muted-foreground text-center py-4">iDEAL not available in your region</p>';
}
}
}, [sdkReady, selectedMethod, amount, currency, item, toast, onPaymentComplete, paymentComplete]);
if (paymentComplete && orderId) {
return (
<div className="border border-primary bg-card p-4 w-full max-w-sm">
<div className="border-b border-primary pb-2 mb-4">
<span className="text-xs text-primary uppercase tracking-wider">
[ PAYMENT CONFIRMED ]
</span>
</div>
<div className="text-center py-6">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-primary/20 flex items-center justify-center">
<Check className="w-8 h-8 text-primary" />
</div>
<h3 className="text-lg font-bold text-foreground mb-2">
Payment Successful!
</h3>
<p className="text-sm text-muted-foreground mb-4">
{amount} {currency} paid
</p>
</div>
<div className="border border-border bg-secondary p-3 mb-4">
<p className="text-xs text-muted-foreground mb-1">Order ID:</p>
<code className="text-xs text-primary break-all font-mono">
{orderId}
</code>
</div>
<p className="text-xs text-muted-foreground text-center">
Your purchase is complete. Check your email for confirmation.
</p>
</div>
);
}
return (
<div className="border border-border bg-card p-4 w-full max-w-sm">
<div className="border-b border-border pb-2 mb-4">
<span className="text-xs text-muted-foreground uppercase tracking-wider">
[ CARD / PAYPAL PAYMENT ]
</span>
</div>
<div className="mb-4">
<p className="text-sm text-foreground mb-1">{item}</p>
<p className="text-xs text-muted-foreground">
Pay securely with PayPal, card, or iDEAL
</p>
</div>
<div className="grid grid-cols-3 gap-1 mb-4">
<Button
variant={selectedMethod === 'paypal' ? "default" : "secondary"}
size="sm"
onClick={() => setSelectedMethod('paypal')}
className="text-xs px-2 py-1 h-auto"
>
PayPal
</Button>
<Button
variant={selectedMethod === 'card' ? "default" : "secondary"}
size="sm"
onClick={() => setSelectedMethod('card')}
className="text-xs px-2 py-1 h-auto flex items-center gap-1"
>
<CreditCard className="h-3 w-3" />
Card
</Button>
<Button
variant={selectedMethod === 'ideal' ? "default" : "secondary"}
size="sm"
onClick={() => setSelectedMethod('ideal')}
className="text-xs px-2 py-1 h-auto"
>
iDEAL
</Button>
</div>
<div className="border-t border-border pt-3 mb-4">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Amount:</span>
<span className="text-foreground font-medium">
{amount} {currency}
</span>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : !sdkReady ? (
<div className="text-center py-4">
<p className="text-xs text-destructive mb-2">
PayPal SDK not loaded
</p>
<p className="text-xs text-muted-foreground">
Replace YOUR_PAYPAL_CLIENT_ID with your actual PayPal Client ID
</p>
</div>
) : (
<div className="space-y-3">
<div
ref={paypalButtonRef}
className={selectedMethod === 'paypal' ? 'block' : 'hidden'}
/>
<div
ref={cardButtonRef}
className={selectedMethod === 'card' ? 'block' : 'hidden'}
/>
<div
ref={idealButtonRef}
className={selectedMethod === 'ideal' ? 'block' : 'hidden'}
/>
</div>
)}
<p className="text-xs text-muted-foreground mt-4 leading-relaxed">
{selectedMethod === 'paypal' && "Pay with your PayPal balance or linked cards."}
{selectedMethod === 'card' && "Pay securely with Visa, Mastercard, or Amex."}
{selectedMethod === 'ideal' && "Pay directly from your Dutch bank account."}
</p>
</div>
);
};
export default PayPalPayment;