1. New Components- src/components/PaymentMethodCard.js - Displays individual payment method- src/components/AddPaymentMethodDialog.js - Stripe Elements dialog for adding cards- src/components/PaymentMethodsSection.js - Main payment methods UI- src/components/PasswordConfirmDialog.js - Admin password re-entry dialog- src/components/admin/AdminPaymentMethodsPanel.js - Admin panel for user payment methods2. Profile Integration (src/pages/Profile.js)- Replaced placeholder Payment Method section with PaymentMethodsSection3. Admin Integration (src/pages/admin/AdminUserView.js)- Added AdminPaymentMethodsPanel to user detail view
This commit is contained in:
222
src/components/AddPaymentMethodDialog.js
Normal file
222
src/components/AddPaymentMethodDialog.js
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from './ui/dialog';
|
||||
import { Button } from './ui/button';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { Label } from './ui/label';
|
||||
import { CreditCard, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import api from '../utils/api';
|
||||
|
||||
/**
|
||||
* AddPaymentMethodDialog - Dialog for adding a new payment method using Stripe Elements
|
||||
*
|
||||
* This dialog should be wrapped in an Elements provider with a clientSecret
|
||||
*
|
||||
* @param {string} saveEndpoint - Optional custom API endpoint for saving (default: '/payment-methods')
|
||||
*/
|
||||
const AddPaymentMethodDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
clientSecret,
|
||||
saveEndpoint = '/payment-methods',
|
||||
}) => {
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [setAsDefault, setSetAsDefault] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get the CardElement
|
||||
const cardElement = elements.getElement(CardElement);
|
||||
|
||||
if (!cardElement) {
|
||||
setError('Card element not found');
|
||||
toast.error('Card element not found');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm the SetupIntent with the card element
|
||||
const { error: stripeError, setupIntent } = await stripe.confirmCardSetup(
|
||||
clientSecret,
|
||||
{
|
||||
payment_method: {
|
||||
card: cardElement,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (stripeError) {
|
||||
setError(stripeError.message);
|
||||
toast.error(stripeError.message);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (setupIntent.status === 'succeeded') {
|
||||
// Save the payment method to our backend using the specified endpoint
|
||||
await api.post(saveEndpoint, {
|
||||
stripe_payment_method_id: setupIntent.payment_method,
|
||||
set_as_default: setAsDefault,
|
||||
});
|
||||
|
||||
toast.success('Payment method added successfully');
|
||||
onSuccess?.();
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
setError(`Setup failed with status: ${setupIntent.status}`);
|
||||
toast.error('Failed to set up payment method');
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to save payment method';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-background rounded-2xl border border-[var(--neutral-800)] p-0 overflow-hidden max-w-md">
|
||||
<DialogHeader className="bg-brand-purple text-white px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<CreditCard className="h-6 w-6" />
|
||||
<div>
|
||||
<DialogTitle
|
||||
className="text-lg font-semibold text-white"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Add Payment Method
|
||||
</DialogTitle>
|
||||
<DialogDescription
|
||||
className="text-white/80 text-sm"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
Enter your card details securely
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||
{/* Stripe Card Element */}
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
className="text-[var(--purple-ink)]"
|
||||
style={{ fontFamily: "'Inter', sans-serif" }}
|
||||
>
|
||||
Card Information
|
||||
</Label>
|
||||
<div className="border border-[var(--neutral-800)] rounded-xl p-4 bg-white">
|
||||
<CardElement
|
||||
options={{
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#2d2a4a',
|
||||
fontFamily: "'Nunito Sans', sans-serif",
|
||||
'::placeholder': {
|
||||
color: '#9ca3af',
|
||||
},
|
||||
},
|
||||
invalid: {
|
||||
color: '#ef4444',
|
||||
},
|
||||
},
|
||||
hidePostalCode: false,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set as Default Checkbox */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id="setAsDefault"
|
||||
checked={setAsDefault}
|
||||
onCheckedChange={setSetAsDefault}
|
||||
className="border-brand-purple data-[state=checked]:bg-brand-purple"
|
||||
/>
|
||||
<Label
|
||||
htmlFor="setAsDefault"
|
||||
className="text-sm text-brand-purple cursor-pointer"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
Set as default payment method for future payments
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-xl">
|
||||
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<p
|
||||
className="text-sm text-red-600"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Security Note */}
|
||||
<p
|
||||
className="text-xs text-brand-purple"
|
||||
style={{ fontFamily: "'Nunito Sans', sans-serif" }}
|
||||
>
|
||||
Your card information is securely processed by Stripe. We never store your full card number.
|
||||
</p>
|
||||
|
||||
<DialogFooter className="flex-row gap-3 justify-end pt-4 border-t border-[var(--neutral-800)]">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading}
|
||||
className="border-2 border-[var(--neutral-800)] text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-6"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!stripe || loading}
|
||||
className="bg-brand-purple text-white hover:bg-[var(--purple-ink)] rounded-full px-6"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Add Card'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPaymentMethodDialog;
|
||||
Reference in New Issue
Block a user