Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | 1x 1x 3x 3x 3x 1x 10x 10x 10x 10x 10x 10x 10x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 10x 7x 1x 5x 5x 5x 5x 5x 5x 1x 4x 1x 3x | import { useState } from 'react'
import { loadStripe, type Stripe } from '@stripe/stripe-js'
import { Elements, useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
import { useTranslation } from 'react-i18next'
import { Button } from '@core/components/ui/button'
import { CardContent, CardFooter } from '@core/components/ui/card'
import type { PaymentProviderProps } from '../../types/payment.types'
// Initialize Stripe outside to avoid recreation
// In a real app config.publishableKey would come from prop, but loadStripe runs once.
// We can use a singleton lazily initialized if key changes.
// Singleton promise to prevent multiple Stripe element initializations
let stripePromise: Promise<Stripe | null> | null = null
// Lazily load Stripe only when needed, reusing the promise
const getStripePromise = (key: string) => {
Eif (!stripePromise) {
stripePromise = loadStripe(key)
}
return stripePromise
}
const StripeFormContent = ({
onSuccess,
onBack,
onError,
}: {
onSuccess: () => void
onBack?: () => void
onError?: (msg: string) => void
}) => {
const stripe = useStripe()
const elements = useElements()
const { t } = useTranslation('common')
const [message, setMessage] = useState<string | null>(null)
const [isProcessing, setIsProcessing] = useState(false)
const [isStripeReady, setIsStripeReady] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
Iif (!stripe || !elements) return
setIsProcessing(true)
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.origin + '/donate',
},
redirect: 'if_required',
})
if (error) {
const msg = error.message || t('donation.error')
setMessage(msg)
Eif (onError) onError(msg)
setIsProcessing(false)
} else {
onSuccess()
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<CardContent>
<PaymentElement onReady={() => setIsStripeReady(true)} />
{message && (
<div className="mt-4 p-3 bg-red-50 text-red-500 rounded text-sm">{message}</div>
)}
</CardContent>
<CardFooter className="flex gap-4">
{onBack && (
<Button
type="button"
variant="outline"
onClick={onBack}
disabled={isProcessing}
>
{t('common.back', 'Back')}
</Button>
)}
<Button
type="submit"
className="w-full"
disabled={!stripe || !elements || isProcessing || !isStripeReady}
style={{
backgroundColor: 'var(--donation-next-button-bg)',
color: 'var(--donation-next-button-text)',
}}
>
{isProcessing ? t('donation.processing') : t('donation.pay_now')}
</Button>
</CardFooter>
</form>
)
}
export const StripePaymentForm = (props: PaymentProviderProps) => {
const { t } = useTranslation('common')
const { sessionData, config } = props
const clientSecret = sessionData?.clientSecret
const configKey = config?.publishableKey as string | undefined
// If config has placeholder, fallback to ENV.
const publishableKey =
(configKey && !configKey.includes('placeholder') ? configKey : undefined) ||
import.meta.env.VITE_STRIPE_PUBLIC_KEY ||
'pk_test_placeholder'
if (!clientSecret) {
return <div className="text-red-500">{t('payment.error_generic')}</div>
}
if (!publishableKey || publishableKey.includes('placeholder')) {
return <div className="text-red-500">{t('payment.error_missing_config')}</div>
}
return (
<Elements
stripe={getStripePromise(publishableKey)}
options={{ clientSecret, appearance: { theme: 'stripe' } }}
>
<StripeFormContent
onSuccess={props.onSuccess}
onBack={props.onBack}
onError={props.onError}
/>
</Elements>
)
}
|