AddNewPaymentMethodModal.tsx146 lines · main
1import HCaptcha from '@hcaptcha/react-hcaptcha'
2import { Elements } from '@stripe/react-stripe-js'
3import { loadStripe } from '@stripe/stripe-js'
4import { useTheme } from 'next-themes'
5import { useCallback, useEffect, useState } from 'react'
6import { toast } from 'sonner'
7import { Modal } from 'ui'
8
9import AddPaymentMethodForm from './AddPaymentMethodForm'
10import { getStripeElementsAppearanceOptions } from './Payment.utils'
11import { useOrganizationPaymentMethodSetupIntent } from '@/data/organizations/organization-payment-method-setup-intent-mutation'
12import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
13import { STRIPE_PUBLIC_KEY } from '@/lib/constants'
14
15interface AddNewPaymentMethodModalProps {
16 visible: boolean
17 returnUrl: string
18 onCancel: () => void
19 onConfirm: () => void
20}
21
22const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
23
24const AddNewPaymentMethodModal = ({
25 visible,
26 returnUrl,
27 onCancel,
28 onConfirm,
29}: AddNewPaymentMethodModalProps) => {
30 const { resolvedTheme } = useTheme()
31 const [intent, setIntent] = useState<any>()
32 const { data: selectedOrganization } = useSelectedOrganizationQuery()
33
34 const [captchaToken, setCaptchaToken] = useState<string | null>(null)
35 const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
36
37 const { mutate: setupIntent } = useOrganizationPaymentMethodSetupIntent({
38 onSuccess: (intent) => {
39 setIntent(intent)
40 },
41 onError: (error) => {
42 toast.error(`Failed to setup intent: ${error.message}`)
43 },
44 })
45
46 const captchaRefCallback = useCallback((node: any) => {
47 setCaptchaRef(node)
48 }, [])
49
50 useEffect(() => {
51 const initSetupIntent = async (hcaptchaToken: string | undefined) => {
52 const slug = selectedOrganization?.slug
53 if (!slug) return console.error('Slug is required')
54 if (!hcaptchaToken) return console.error('HCaptcha token required')
55
56 setIntent(undefined)
57 setupIntent({ slug, hcaptchaToken })
58 }
59
60 const loadPaymentForm = async () => {
61 if (visible && captchaRef) {
62 let token = captchaToken
63
64 try {
65 if (!token) {
66 const captchaResponse = await captchaRef.execute({ async: true })
67 token = captchaResponse?.response ?? null
68 }
69 } catch (error) {
70 return
71 }
72
73 await initSetupIntent(token ?? undefined)
74 resetCaptcha()
75 }
76 }
77
78 loadPaymentForm()
79 }, [visible, captchaRef])
80
81 const resetCaptcha = () => {
82 setCaptchaToken(null)
83 captchaRef?.resetCaptcha()
84 }
85
86 const options = {
87 clientSecret: intent ? intent.client_secret : '',
88 appearance: getStripeElementsAppearanceOptions(resolvedTheme),
89 } as any
90
91 const onLocalCancel = () => {
92 setIntent(undefined)
93 return onCancel()
94 }
95
96 const onLocalConfirm = () => {
97 setIntent(undefined)
98 return onConfirm()
99 }
100
101 return (
102 // We cant display the hCaptcha in the modal, as the modal auto-closes when clicking the captcha
103 // So we only show the modal if the captcha has been executed successfully (intent loaded)
104 <>
105 <HCaptcha
106 ref={captchaRefCallback}
107 sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
108 size="invisible"
109 onOpen={() => {
110 // [Joshen] This is to ensure that hCaptcha popup remains clickable
111 if (document !== undefined) document.body.classList.add('pointer-events-auto!')
112 }}
113 onClose={() => {
114 onLocalCancel()
115 if (document !== undefined) document.body.classList.remove('pointer-events-auto!')
116 }}
117 onVerify={(token) => {
118 setCaptchaToken(token)
119 if (document !== undefined) document.body.classList.remove('pointer-events-auto!')
120 }}
121 onExpire={() => {
122 setCaptchaToken(null)
123 }}
124 />
125
126 <Modal
127 hideFooter
128 size="medium"
129 visible={visible && intent !== undefined}
130 header="Add new payment method"
131 onCancel={onLocalCancel}
132 className="PAYMENT"
133 >
134 <Elements stripe={stripePromise} options={options}>
135 <AddPaymentMethodForm
136 returnUrl={returnUrl}
137 onCancel={onLocalCancel}
138 onConfirm={onLocalConfirm}
139 />
140 </Elements>
141 </Modal>
142 </>
143 )
144}
145
146export default AddNewPaymentMethodModal