feat: Stripe integration with subscription tiers and donations (#55) #57
@@ -14,12 +14,12 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
Chip,
|
Chip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
import { CardElement, AddressElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Card } from '../../../shared-minimal/components/Card';
|
import { Card } from '../../../shared-minimal/components/Card';
|
||||||
import { useCreateDonation, useDonations } from '../hooks/useSubscription';
|
import { useCreateDonation, useDonations } from '../hooks/useSubscription';
|
||||||
import type { StripeCardElementChangeEvent } from '@stripe/stripe-js';
|
import type { StripeCardElementChangeEvent, StripeAddressElementChangeEvent } from '@stripe/stripe-js';
|
||||||
import type { SubscriptionTier } from '../types/subscription.types';
|
import type { SubscriptionTier } from '../types/subscription.types';
|
||||||
|
|
||||||
interface DonationSectionProps {
|
interface DonationSectionProps {
|
||||||
@@ -48,6 +48,7 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [cardComplete, setCardComplete] = useState(false);
|
const [cardComplete, setCardComplete] = useState(false);
|
||||||
|
const [addressComplete, setAddressComplete] = useState(false);
|
||||||
const [showSuccess, setShowSuccess] = useState(false);
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
|
||||||
const createDonationMutation = useCreateDonation();
|
const createDonationMutation = useCreateDonation();
|
||||||
@@ -60,6 +61,10 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
setCardComplete(event.complete);
|
setCardComplete(event.complete);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddressChange = (event: StripeAddressElementChangeEvent) => {
|
||||||
|
setAddressComplete(event.complete);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent) => {
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -72,6 +77,11 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addressElement = elements.getElement(AddressElement);
|
||||||
|
if (!addressElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate amount
|
// Validate amount
|
||||||
const amountNum = parseFloat(amount);
|
const amountNum = parseFloat(amount);
|
||||||
if (isNaN(amountNum) || amountNum < 0.5) {
|
if (isNaN(amountNum) || amountNum < 0.5) {
|
||||||
@@ -79,6 +89,13 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get billing address data
|
||||||
|
const addressData = await addressElement.getValue();
|
||||||
|
if (!addressData.complete) {
|
||||||
|
setError('Please complete the billing address');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -87,10 +104,21 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
const donationResponse = await createDonationMutation.mutateAsync(amountNum);
|
const donationResponse = await createDonationMutation.mutateAsync(amountNum);
|
||||||
const { clientSecret } = donationResponse.data;
|
const { clientSecret } = donationResponse.data;
|
||||||
|
|
||||||
// Confirm payment with Stripe
|
// Confirm payment with Stripe including billing details
|
||||||
const { error: confirmError } = await stripe.confirmCardPayment(clientSecret, {
|
const { error: confirmError } = await stripe.confirmCardPayment(clientSecret, {
|
||||||
payment_method: {
|
payment_method: {
|
||||||
card: cardElement,
|
card: cardElement,
|
||||||
|
billing_details: {
|
||||||
|
name: addressData.value.name,
|
||||||
|
address: {
|
||||||
|
line1: addressData.value.address.line1,
|
||||||
|
line2: addressData.value.address.line2 || undefined,
|
||||||
|
city: addressData.value.address.city,
|
||||||
|
state: addressData.value.address.state,
|
||||||
|
postal_code: addressData.value.address.postal_code,
|
||||||
|
country: addressData.value.address.country,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,7 +146,7 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && !processing;
|
const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && addressComplete && !processing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
@@ -164,6 +192,35 @@ export const DonationSection: React.FC<DonationSectionProps> = ({ currentTier })
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Billing Address
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 2,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddressElement
|
||||||
|
options={{
|
||||||
|
mode: 'billing',
|
||||||
|
defaultValues: {
|
||||||
|
address: {
|
||||||
|
country: 'US',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={handleAddressChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
Card Details
|
Card Details
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
import { CardElement, AddressElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
||||||
import { useCreateDonation, useDonations } from '../hooks/useSubscription';
|
import { useCreateDonation, useDonations } from '../hooks/useSubscription';
|
||||||
import type { StripeCardElementChangeEvent } from '@stripe/stripe-js';
|
import type { StripeCardElementChangeEvent, StripeAddressElementChangeEvent } from '@stripe/stripe-js';
|
||||||
import type { SubscriptionTier } from '../types/subscription.types';
|
import type { SubscriptionTier } from '../types/subscription.types';
|
||||||
|
|
||||||
interface DonationSectionMobileProps {
|
interface DonationSectionMobileProps {
|
||||||
@@ -33,6 +33,7 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [cardComplete, setCardComplete] = useState(false);
|
const [cardComplete, setCardComplete] = useState(false);
|
||||||
|
const [addressComplete, setAddressComplete] = useState(false);
|
||||||
const [showSuccess, setShowSuccess] = useState(false);
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
|
||||||
const createDonationMutation = useCreateDonation();
|
const createDonationMutation = useCreateDonation();
|
||||||
@@ -45,6 +46,10 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
setCardComplete(event.complete);
|
setCardComplete(event.complete);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddressChange = (event: StripeAddressElementChangeEvent) => {
|
||||||
|
setAddressComplete(event.complete);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent) => {
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -57,6 +62,11 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addressElement = elements.getElement(AddressElement);
|
||||||
|
if (!addressElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate amount
|
// Validate amount
|
||||||
const amountNum = parseFloat(amount);
|
const amountNum = parseFloat(amount);
|
||||||
if (isNaN(amountNum) || amountNum < 0.5) {
|
if (isNaN(amountNum) || amountNum < 0.5) {
|
||||||
@@ -64,6 +74,13 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get billing address data
|
||||||
|
const addressData = await addressElement.getValue();
|
||||||
|
if (!addressData.complete) {
|
||||||
|
setError('Please complete the billing address');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -72,10 +89,21 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
const donationResponse = await createDonationMutation.mutateAsync(amountNum);
|
const donationResponse = await createDonationMutation.mutateAsync(amountNum);
|
||||||
const { clientSecret } = donationResponse.data;
|
const { clientSecret } = donationResponse.data;
|
||||||
|
|
||||||
// Confirm payment with Stripe
|
// Confirm payment with Stripe including billing details
|
||||||
const { error: confirmError } = await stripe.confirmCardPayment(clientSecret, {
|
const { error: confirmError } = await stripe.confirmCardPayment(clientSecret, {
|
||||||
payment_method: {
|
payment_method: {
|
||||||
card: cardElement,
|
card: cardElement,
|
||||||
|
billing_details: {
|
||||||
|
name: addressData.value.name,
|
||||||
|
address: {
|
||||||
|
line1: addressData.value.address.line1,
|
||||||
|
line2: addressData.value.address.line2 || undefined,
|
||||||
|
city: addressData.value.address.city,
|
||||||
|
state: addressData.value.address.state,
|
||||||
|
postal_code: addressData.value.address.postal_code,
|
||||||
|
country: addressData.value.address.country,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,7 +131,7 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && !processing;
|
const isFormValid = amount && parseFloat(amount) >= 0.5 && cardComplete && addressComplete && !processing;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlassCard>
|
<GlassCard>
|
||||||
@@ -151,6 +179,25 @@ export const DonationSectionMobile: React.FC<DonationSectionMobileProps> = ({ cu
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-avus mb-2">
|
||||||
|
Billing Address
|
||||||
|
</label>
|
||||||
|
<div className="border border-slate-200 dark:border-grigio rounded-xl p-3 bg-white dark:bg-nero">
|
||||||
|
<AddressElement
|
||||||
|
options={{
|
||||||
|
mode: 'billing',
|
||||||
|
defaultValues: {
|
||||||
|
address: {
|
||||||
|
country: 'US',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={handleAddressChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-avus mb-2">
|
<label className="block text-sm font-medium text-slate-700 dark:text-avus mb-2">
|
||||||
Card Details
|
Card Details
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Box, Button, Typography, Alert, CircularProgress } from '@mui/material';
|
import { Box, Button, Typography, Alert, CircularProgress } from '@mui/material';
|
||||||
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
import { PaymentElement, AddressElement, useStripe, useElements } from '@stripe/react-stripe-js';
|
||||||
import type { StripeCardElementChangeEvent } from '@stripe/stripe-js';
|
import type { StripePaymentElementChangeEvent, StripeAddressElementChangeEvent } from '@stripe/stripe-js';
|
||||||
|
|
||||||
interface PaymentMethodFormProps {
|
interface PaymentMethodFormProps {
|
||||||
onSubmit: (paymentMethodId: string) => void;
|
onSubmit: (paymentMethodId: string) => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CARD_ELEMENT_OPTIONS = {
|
|
||||||
style: {
|
|
||||||
base: {
|
|
||||||
fontSize: '16px',
|
|
||||||
color: '#424770',
|
|
||||||
'::placeholder': {
|
|
||||||
color: '#aab7c4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
invalid: {
|
|
||||||
color: '#9e2146',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
@@ -31,11 +16,15 @@ export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
|||||||
const elements = useElements();
|
const elements = useElements();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [cardComplete, setCardComplete] = useState(false);
|
const [paymentComplete, setPaymentComplete] = useState(false);
|
||||||
|
const [addressComplete, setAddressComplete] = useState(false);
|
||||||
|
|
||||||
const handleCardChange = (event: StripeCardElementChangeEvent) => {
|
const handlePaymentChange = (event: StripePaymentElementChangeEvent) => {
|
||||||
setError(event.error?.message || null);
|
setPaymentComplete(event.complete);
|
||||||
setCardComplete(event.complete);
|
};
|
||||||
|
|
||||||
|
const handleAddressChange = (event: StripeAddressElementChangeEvent) => {
|
||||||
|
setAddressComplete(event.complete);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent) => {
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
@@ -45,18 +34,49 @@ export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardElement = elements.getElement(CardElement);
|
|
||||||
if (!cardElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProcessing(true);
|
setProcessing(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Submit the elements to validate all fields
|
||||||
|
const { error: submitError } = await elements.submit();
|
||||||
|
if (submitError) {
|
||||||
|
setError(submitError.message || 'Failed to validate payment details');
|
||||||
|
setProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get billing details from AddressElement
|
||||||
|
const addressElement = elements.getElement(AddressElement);
|
||||||
|
if (!addressElement) {
|
||||||
|
setError('Address element not found');
|
||||||
|
setProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressData = await addressElement.getValue();
|
||||||
|
if (!addressData.complete) {
|
||||||
|
setError('Please complete the billing address');
|
||||||
|
setProcessing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the payment method with billing details
|
||||||
const { error: createError, paymentMethod } = await stripe.createPaymentMethod({
|
const { error: createError, paymentMethod } = await stripe.createPaymentMethod({
|
||||||
type: 'card',
|
elements,
|
||||||
card: cardElement,
|
params: {
|
||||||
|
billing_details: {
|
||||||
|
name: addressData.value.name,
|
||||||
|
address: {
|
||||||
|
line1: addressData.value.address.line1,
|
||||||
|
line2: addressData.value.address.line2 || undefined,
|
||||||
|
city: addressData.value.address.city,
|
||||||
|
state: addressData.value.address.state,
|
||||||
|
postal_code: addressData.value.address.postal_code,
|
||||||
|
country: addressData.value.address.country,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (createError) {
|
if (createError) {
|
||||||
@@ -74,9 +94,39 @@ export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFormComplete = paymentComplete && addressComplete;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Billing Address
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 1,
|
||||||
|
p: 2,
|
||||||
|
mb: 3,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddressElement
|
||||||
|
options={{
|
||||||
|
mode: 'billing',
|
||||||
|
defaultValues: {
|
||||||
|
address: {
|
||||||
|
country: 'US',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={handleAddressChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
Card Details
|
Card Details
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -91,7 +141,17 @@ export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardElement options={CARD_ELEMENT_OPTIONS} onChange={handleCardChange} />
|
<PaymentElement
|
||||||
|
options={{
|
||||||
|
layout: 'tabs',
|
||||||
|
fields: {
|
||||||
|
billingDetails: {
|
||||||
|
address: 'never',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={handlePaymentChange}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -106,7 +166,7 @@ export const PaymentMethodForm: React.FC<PaymentMethodFormProps> = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!stripe || processing || isLoading || !cardComplete}
|
disabled={!stripe || processing || isLoading || !isFormComplete}
|
||||||
>
|
>
|
||||||
{processing || isLoading ? (
|
{processing || isLoading ? (
|
||||||
<CircularProgress size={24} />
|
<CircularProgress size={24} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Elements } from '@stripe/react-stripe-js';
|
import { Elements } from '@stripe/react-stripe-js';
|
||||||
import { loadStripe } from '@stripe/stripe-js';
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
|
import type { StripeElementsOptions } from '@stripe/stripe-js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
import { GlassCard } from '../../../shared-minimal/components/mobile/GlassCard';
|
||||||
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
import { MobileContainer } from '../../../shared-minimal/components/mobile/MobileContainer';
|
||||||
@@ -18,6 +19,12 @@ import type { BillingCycle, SubscriptionTier, SubscriptionPlan } from '../types/
|
|||||||
|
|
||||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '');
|
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '');
|
||||||
|
|
||||||
|
const paymentElementsOptions: StripeElementsOptions = {
|
||||||
|
mode: 'setup',
|
||||||
|
currency: 'usd',
|
||||||
|
paymentMethodCreation: 'manual',
|
||||||
|
};
|
||||||
|
|
||||||
interface MobileTierCardProps {
|
interface MobileTierCardProps {
|
||||||
plan: SubscriptionPlan;
|
plan: SubscriptionPlan;
|
||||||
billingCycle: BillingCycle;
|
billingCycle: BillingCycle;
|
||||||
@@ -358,7 +365,7 @@ export const SubscriptionMobileScreen: React.FC = () => {
|
|||||||
onClose={() => !checkoutMutation.isPending && setShowPaymentDialog(false)}
|
onClose={() => !checkoutMutation.isPending && setShowPaymentDialog(false)}
|
||||||
title={`Upgrade to ${selectedTier && PLANS.find((p) => p.tier === selectedTier)?.name}`}
|
title={`Upgrade to ${selectedTier && PLANS.find((p) => p.tier === selectedTier)?.name}`}
|
||||||
>
|
>
|
||||||
<Elements stripe={stripePromise}>
|
<Elements stripe={stripePromise} options={paymentElementsOptions}>
|
||||||
<PaymentMethodForm
|
<PaymentMethodForm
|
||||||
onSubmit={handlePaymentSubmit}
|
onSubmit={handlePaymentSubmit}
|
||||||
isLoading={checkoutMutation.isPending}
|
isLoading={checkoutMutation.isPending}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Elements } from '@stripe/react-stripe-js';
|
import { Elements } from '@stripe/react-stripe-js';
|
||||||
import { loadStripe } from '@stripe/stripe-js';
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
|
import type { StripeElementsOptions } from '@stripe/stripe-js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Card } from '../../../shared-minimal/components/Card';
|
import { Card } from '../../../shared-minimal/components/Card';
|
||||||
import { TierCard } from '../components/TierCard';
|
import { TierCard } from '../components/TierCard';
|
||||||
@@ -36,6 +37,12 @@ import type { BillingCycle, SubscriptionTier } from '../types/subscription.types
|
|||||||
|
|
||||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '');
|
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '');
|
||||||
|
|
||||||
|
const paymentElementsOptions: StripeElementsOptions = {
|
||||||
|
mode: 'setup',
|
||||||
|
currency: 'usd',
|
||||||
|
paymentMethodCreation: 'manual',
|
||||||
|
};
|
||||||
|
|
||||||
export const SubscriptionPage: React.FC = () => {
|
export const SubscriptionPage: React.FC = () => {
|
||||||
const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly');
|
const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly');
|
||||||
const [selectedTier, setSelectedTier] = useState<SubscriptionTier | null>(null);
|
const [selectedTier, setSelectedTier] = useState<SubscriptionTier | null>(null);
|
||||||
@@ -279,7 +286,7 @@ export const SubscriptionPage: React.FC = () => {
|
|||||||
Upgrade to {selectedTier && PLANS.find((p) => p.tier === selectedTier)?.name}
|
Upgrade to {selectedTier && PLANS.find((p) => p.tier === selectedTier)?.name}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Elements stripe={stripePromise}>
|
<Elements stripe={stripePromise} options={paymentElementsOptions}>
|
||||||
<PaymentMethodForm
|
<PaymentMethodForm
|
||||||
onSubmit={handlePaymentSubmit}
|
onSubmit={handlePaymentSubmit}
|
||||||
isLoading={checkoutMutation.isPending}
|
isLoading={checkoutMutation.isPending}
|
||||||
|
|||||||
Reference in New Issue
Block a user