AddNewPaymentMethodModal.tsx 4.3 KB

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