120 lines
2.8 KiB
TypeScript
120 lines
2.8 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Box, Button, Typography, Alert, CircularProgress } from '@mui/material';
|
|
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
|
import type { StripeCardElementChangeEvent } from '@stripe/stripe-js';
|
|
|
|
interface PaymentMethodFormProps {
|
|
onSubmit: (paymentMethodId: string) => void;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
const CARD_ELEMENT_OPTIONS = {
|
|
style: {
|
|
base: {
|
|
fontSize: '16px',
|
|
color: '#424770',
|
|
'::placeholder': {
|
|
color: '#aab7c4',
|
|
},
|
|
},
|
|
invalid: {
|
|
color: '#9e2146',
|
|
},
|
|
},
|
|
};
|
|
|
|
export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
|
onSubmit,
|
|
isLoading = false,
|
|
}) => {
|
|
const stripe = useStripe();
|
|
const elements = useElements();
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [processing, setProcessing] = useState(false);
|
|
const [cardComplete, setCardComplete] = useState(false);
|
|
|
|
const handleCardChange = (event: StripeCardElementChangeEvent) => {
|
|
setError(event.error?.message || null);
|
|
setCardComplete(event.complete);
|
|
};
|
|
|
|
const handleSubmit = async (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
|
|
if (!stripe || !elements) {
|
|
return;
|
|
}
|
|
|
|
const cardElement = elements.getElement(CardElement);
|
|
if (!cardElement) {
|
|
return;
|
|
}
|
|
|
|
setProcessing(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const { error: createError, paymentMethod } = await stripe.createPaymentMethod({
|
|
type: 'card',
|
|
card: cardElement,
|
|
});
|
|
|
|
if (createError) {
|
|
setError(createError.message || 'Failed to create payment method');
|
|
setProcessing(false);
|
|
return;
|
|
}
|
|
|
|
if (paymentMethod) {
|
|
onSubmit(paymentMethod.id);
|
|
}
|
|
} catch {
|
|
setError('An unexpected error occurred');
|
|
setProcessing(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit}>
|
|
<Box sx={{ mb: 3 }}>
|
|
<Typography variant="subtitle2" gutterBottom>
|
|
Card Details
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
border: 1,
|
|
borderColor: 'divider',
|
|
borderRadius: 1,
|
|
p: 2,
|
|
'&:hover': {
|
|
borderColor: 'primary.main',
|
|
},
|
|
}}
|
|
>
|
|
<CardElement options={CARD_ELEMENT_OPTIONS} onChange={handleCardChange} />
|
|
</Box>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
color="primary"
|
|
fullWidth
|
|
disabled={!stripe || processing || isLoading || !cardComplete}
|
|
>
|
|
{processing || isLoading ? (
|
|
<CircularProgress size={24} />
|
|
) : (
|
|
'Update Payment Method'
|
|
)}
|
|
</Button>
|
|
</form>
|
|
);
|
|
};
|