b42ff297eb
- 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
311 lines
9.4 KiB
TypeScript
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}¤cy=${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;
|
|
|