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