![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/components/payments/ |
'use client';
import React, { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import { useSession } from 'next-auth/react';
import { toast } from 'react-hot-toast';
import {
CreditCard,
Plus,
Edit,
Trash2,
Shield,
CheckCircle,
AlertCircle,
Lock
} from 'lucide-react';
// Initialize Stripe
const getStripePromise = () => {
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
if (!publishableKey ||
publishableKey === 'pk_test_your_stripe_publishable_key_here' ||
publishableKey === 'pk_live_your_stripe_publishable_key_here') {
console.warn('Stripe publishable key not properly configured.');
return null;
}
return loadStripe(publishableKey);
};
const stripePromise = getStripePromise();
interface PaymentMethod {
id: string;
type: string;
last4: string;
brand: string;
expMonth: number;
expYear: number;
isDefault: boolean;
country?: string;
fingerprint?: string;
}
interface PaymentMethodsManagerProps {
onMethodAdded?: (method: PaymentMethod) => void;
onMethodRemoved?: (methodId: string) => void;
onMethodUpdated?: (method: PaymentMethod) => void;
}
const PaymentMethodsManager: React.FC<PaymentMethodsManagerProps> = ({
onMethodAdded,
onMethodRemoved,
onMethodUpdated
}) => {
const { data: session } = useSession();
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
const [loading, setLoading] = useState(true);
const [showAddForm, setShowAddForm] = useState(false);
const [editingMethod, setEditingMethod] = useState<PaymentMethod | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
fetchPaymentMethods();
}, []);
const fetchPaymentMethods = async () => {
try {
setLoading(true);
const response = await fetch('/api/user/payment-methods');
if (response.ok) {
const data = await response.json();
setPaymentMethods(data.paymentMethods || []);
} else {
console.error('Failed to fetch payment methods');
}
} catch (error) {
console.error('Error fetching payment methods:', error);
toast.error('Failed to load payment methods');
} finally {
setLoading(false);
}
};
const handleAddMethod = async (paymentMethod: any) => {
try {
setIsProcessing(true);
const response = await fetch('/api/user/payment-methods', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paymentMethodId: paymentMethod.id
})
});
if (response.ok) {
const newMethod = await response.json();
setPaymentMethods(prev => [...prev, newMethod.paymentMethod]);
setShowAddForm(false);
toast.success('Payment method added successfully');
onMethodAdded?.(newMethod.paymentMethod);
} else {
const error = await response.json();
toast.error(error.message || 'Failed to add payment method');
}
} catch (error) {
console.error('Error adding payment method:', error);
toast.error('Failed to add payment method');
} finally {
setIsProcessing(false);
}
};
const handleRemoveMethod = async (methodId: string) => {
if (!confirm('Are you sure you want to remove this payment method?')) {
return;
}
try {
const response = await fetch(`/api/user/payment-methods/${methodId}`, {
method: 'DELETE'
});
if (response.ok) {
setPaymentMethods(prev => prev.filter(method => method.id !== methodId));
toast.success('Payment method removed successfully');
onMethodRemoved?.(methodId);
} else {
const error = await response.json();
toast.error(error.message || 'Failed to remove payment method');
}
} catch (error) {
console.error('Error removing payment method:', error);
toast.error('Failed to remove payment method');
}
};
const handleSetDefault = async (methodId: string) => {
try {
const response = await fetch(`/api/user/payment-methods/${methodId}/default`, {
method: 'POST'
});
if (response.ok) {
setPaymentMethods(prev =>
prev.map(method => ({
...method,
isDefault: method.id === methodId
}))
);
toast.success('Default payment method updated');
onMethodUpdated?.(paymentMethods.find(m => m.id === methodId)!);
} else {
const error = await response.json();
toast.error(error.message || 'Failed to update default payment method');
}
} catch (error) {
console.error('Error setting default payment method:', error);
toast.error('Failed to update default payment method');
}
};
const getCardIcon = (brand: string) => {
switch (brand.toLowerCase()) {
case 'visa':
return '💳';
case 'mastercard':
return '💳';
case 'amex':
return '💳';
case 'discover':
return '💳';
default:
return '💳';
}
};
const formatExpiry = (month: number, year: number) => {
return `${month.toString().padStart(2, '0')}/${year.toString().slice(-2)}`;
};
if (!stripePromise) {
return (
<div className="text-center p-8">
<AlertCircle className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">Payment System Unavailable</h3>
<p className="mt-1 text-sm text-gray-500">
Stripe payment processing is not properly configured.
</p>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading payment methods...</span>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-gray-900">Payment Methods</h2>
<p className="text-gray-600">Manage your saved payment methods</p>
</div>
<button
onClick={() => setShowAddForm(true)}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<Plus className="h-4 w-4 mr-2" />
Add Payment Method
</button>
</div>
{/* Payment Methods List */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{paymentMethods.map((method) => (
<div
key={method.id}
className={`bg-white rounded-lg border-2 p-6 ${
method.isDefault ? 'border-blue-500 bg-blue-50' : 'border-gray-200'
}`}
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center">
<span className="text-2xl mr-2">{getCardIcon(method.brand)}</span>
<div>
<p className="font-medium text-gray-900">
{method.brand.charAt(0).toUpperCase() + method.brand.slice(1)} •••• {method.last4}
</p>
<p className="text-sm text-gray-500">
Expires {formatExpiry(method.expMonth, method.expYear)}
</p>
</div>
</div>
{method.isDefault && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<CheckCircle className="h-3 w-3 mr-1" />
Default
</span>
)}
</div>
<div className="flex items-center justify-between">
<div className="flex space-x-2">
{!method.isDefault && (
<button
onClick={() => handleSetDefault(method.id)}
className="text-sm text-blue-600 hover:text-blue-800"
>
Set Default
</button>
)}
<button
onClick={() => setEditingMethod(method)}
className="text-sm text-gray-600 hover:text-gray-800"
>
<Edit className="h-4 w-4" />
</button>
</div>
<button
onClick={() => handleRemoveMethod(method.id)}
className="text-sm text-red-600 hover:text-red-800"
disabled={method.isDefault}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
{/* Empty State */}
{paymentMethods.length === 0 && !showAddForm && (
<div className="text-center py-12">
<CreditCard className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No payment methods</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by adding a payment method for faster checkout.
</p>
<div className="mt-6">
<button
onClick={() => setShowAddForm(true)}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
<Plus className="h-4 w-4 mr-2" />
Add Payment Method
</button>
</div>
</div>
)}
{/* Add Payment Method Form */}
{showAddForm && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<h3 className="text-lg font-medium text-gray-900 mb-4">Add Payment Method</h3>
<Elements stripe={stripePromise}>
<AddPaymentMethodForm
onSuccess={handleAddMethod}
onCancel={() => setShowAddForm(false)}
isProcessing={isProcessing}
/>
</Elements>
</div>
</div>
</div>
)}
{/* Edit Payment Method Form */}
{editingMethod && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<h3 className="text-lg font-medium text-gray-900 mb-4">Edit Payment Method</h3>
<EditPaymentMethodForm
method={editingMethod}
onSuccess={(updatedMethod) => {
setPaymentMethods(prev =>
prev.map(m => m.id === updatedMethod.id ? updatedMethod : m)
);
setEditingMethod(null);
onMethodUpdated?.(updatedMethod);
}}
onCancel={() => setEditingMethod(null)}
/>
</div>
</div>
</div>
)}
{/* Security Notice */}
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
<div className="flex">
<Shield className="h-5 w-5 text-blue-400 mt-0.5" />
<div className="ml-3">
<h3 className="text-sm font-medium text-blue-800">Secure Payment Storage</h3>
<div className="mt-2 text-sm text-blue-700">
<p>
Your payment information is securely stored and encrypted. We never store your full card details on our servers.
</p>
</div>
</div>
</div>
</div>
</div>
);
};
// Add Payment Method Form Component
interface AddPaymentMethodFormProps {
onSuccess: (paymentMethod: any) => void;
onCancel: () => void;
isProcessing: boolean;
}
function AddPaymentMethodForm({ onSuccess, onCancel, isProcessing }: AddPaymentMethodFormProps) {
const stripe = useStripe();
const elements = useElements();
const { data: session } = useSession();
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (!stripe || !elements) {
toast.error('Payment system not ready');
return;
}
const cardElement = elements.getElement(CardElement);
if (!cardElement) {
toast.error('Card element not found');
return;
}
try {
// Create payment method
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
billing_details: {
name: session?.user?.name || 'Anonymous',
email: session?.user?.email || undefined,
},
});
if (error) {
toast.error(error.message || 'Failed to create payment method');
} else if (paymentMethod) {
onSuccess(paymentMethod);
}
} catch (error) {
console.error('Error creating payment method:', error);
toast.error('Failed to create payment method');
}
};
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
fontFamily: 'Inter, system-ui, sans-serif',
},
invalid: {
color: '#9e2146',
},
},
hidePostalCode: false,
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Card Information
</label>
<div className="p-3 border border-gray-300 rounded-md focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500">
<CardElement options={cardElementOptions} />
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
Cancel
</button>
<button
type="submit"
disabled={!stripe || isProcessing}
className={`px-4 py-2 text-sm font-medium rounded-md text-white ${
isProcessing || !stripe
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{isProcessing ? 'Adding...' : 'Add Payment Method'}
</button>
</div>
</form>
);
}
// Edit Payment Method Form Component
interface EditPaymentMethodFormProps {
method: PaymentMethod;
onSuccess: (method: PaymentMethod) => void;
onCancel: () => void;
}
function EditPaymentMethodForm({ method, onSuccess, onCancel }: EditPaymentMethodFormProps) {
const [isDefault, setIsDefault] = useState(method.isDefault);
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
try {
const response = await fetch(`/api/user/payment-methods/${method.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
isDefault
})
});
if (response.ok) {
const updatedMethod = { ...method, isDefault };
onSuccess(updatedMethod);
toast.success('Payment method updated successfully');
} else {
const error = await response.json();
toast.error(error.message || 'Failed to update payment method');
}
} catch (error) {
console.error('Error updating payment method:', error);
toast.error('Failed to update payment method');
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="bg-gray-50 rounded-md p-4">
<div className="flex items-center mb-3">
<span className="text-2xl mr-2">💳</span>
<div>
<p className="font-medium text-gray-900">
{method.brand.charAt(0).toUpperCase() + method.brand.slice(1)} •••• {method.last4}
</p>
<p className="text-sm text-gray-500">
Expires {method.expMonth.toString().padStart(2, '0')}/{method.expYear.toString().slice(-2)}
</p>
</div>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isDefault"
checked={isDefault}
onChange={(e) => setIsDefault(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="isDefault" className="ml-2 block text-sm text-gray-900">
Set as default payment method
</label>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
>
Update Payment Method
</button>
</div>
</form>
);
}
export default PaymentMethodsManager;