index.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import HCaptcha from '@hcaptcha/react-hcaptcha'
  2. import Head from 'next/head'
  3. import { useCallback, useEffect, useState } from 'react'
  4. import { NewOrgForm } from '@/components/interfaces/Organization/NewOrg/NewOrgForm'
  5. import { AppLayout } from '@/components/layouts/AppLayout/AppLayout'
  6. import { DefaultLayout } from '@/components/layouts/DefaultLayout'
  7. import WizardLayout from '@/components/layouts/WizardLayout'
  8. import { SetupIntentResponse, useSetupIntent } from '@/data/stripe/setup-intent-mutation'
  9. import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
  10. import { buildStudioPageTitle } from '@/lib/page-title'
  11. import type { NextPageWithLayout } from '@/types'
  12. /**
  13. * No org selected yet, create a new one
  14. */
  15. const Wizard: NextPageWithLayout = () => {
  16. const [intent, setIntent] = useState<SetupIntentResponse>()
  17. const { appTitle } = useCustomContent(['app:title'])
  18. const pageTitle = buildStudioPageTitle({
  19. section: 'New Organization',
  20. brand: appTitle || 'Briven',
  21. })
  22. const [captchaToken, setCaptchaToken] = useState<string | null>(null)
  23. const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
  24. const [selectedPlan, setSelectedPlan] = useState<string | null>(null)
  25. const { mutate: setupIntent } = useSetupIntent({ onSuccess: (res) => setIntent(res) })
  26. const captchaRefCallback = useCallback((node: any) => {
  27. setCaptchaRef(node)
  28. }, [])
  29. const initSetupIntent = async (hcaptchaToken: string | undefined) => {
  30. if (!hcaptchaToken) return console.error('Hcaptcha token is required')
  31. // Force a reload of Elements, necessary for Stripe
  32. // Also mitigates card testing to some extent as we generate a new captcha token
  33. setIntent(undefined)
  34. setupIntent({ hcaptchaToken })
  35. }
  36. const loadPaymentForm = async (force = false) => {
  37. if (selectedPlan == null || selectedPlan === 'FREE') return
  38. if (intent != null && !force) return
  39. if (captchaRef) {
  40. let token = captchaToken
  41. try {
  42. if (!token) {
  43. const captchaResponse = await captchaRef.execute({ async: true })
  44. token = captchaResponse?.response ?? null
  45. }
  46. } catch (error) {
  47. return
  48. }
  49. await initSetupIntent(token ?? undefined)
  50. resetCaptcha()
  51. }
  52. }
  53. useEffect(() => {
  54. loadPaymentForm()
  55. }, [captchaRef, selectedPlan])
  56. const resetSetupIntent = () => {
  57. setIntent(undefined)
  58. return loadPaymentForm(true)
  59. }
  60. const onLocalCancel = () => {
  61. setIntent(undefined)
  62. }
  63. const resetCaptcha = () => {
  64. setCaptchaToken(null)
  65. captchaRef?.resetCaptcha()
  66. }
  67. return (
  68. <>
  69. {/* Wizard layouts set the visual header but not the browser tab title. */}
  70. <Head>
  71. <title>{pageTitle}</title>
  72. <meta name="description" content="Briven Studio" />
  73. </Head>
  74. <HCaptcha
  75. ref={captchaRefCallback}
  76. sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
  77. size="invisible"
  78. onVerify={(token) => {
  79. setCaptchaToken(token)
  80. }}
  81. onClose={onLocalCancel}
  82. onExpire={() => {
  83. setCaptchaToken(null)
  84. }}
  85. />
  86. <NewOrgForm
  87. setupIntent={intent}
  88. onPaymentMethodReset={() => resetSetupIntent()}
  89. onPlanSelected={(plan) => setSelectedPlan(plan)}
  90. />
  91. </>
  92. )
  93. }
  94. Wizard.getLayout = (page) => (
  95. <AppLayout>
  96. <DefaultLayout hideMobileMenu headerTitle="New organization">
  97. <WizardLayout>{page}</WizardLayout>
  98. </DefaultLayout>
  99. </AppLayout>
  100. )
  101. export default Wizard