feat: add frontend subscription page - M4 (refs #55)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user