index.tsx121 lines · main
| 1 | import HCaptcha from '@hcaptcha/react-hcaptcha' |
| 2 | import Head from 'next/head' |
| 3 | import { useCallback, useEffect, useState } from 'react' |
| 4 | |
| 5 | import { NewOrgForm } from '@/components/interfaces/Organization/NewOrg/NewOrgForm' |
| 6 | import { AppLayout } from '@/components/layouts/AppLayout/AppLayout' |
| 7 | import { DefaultLayout } from '@/components/layouts/DefaultLayout' |
| 8 | import WizardLayout from '@/components/layouts/WizardLayout' |
| 9 | import { SetupIntentResponse, useSetupIntent } from '@/data/stripe/setup-intent-mutation' |
| 10 | import { useCustomContent } from '@/hooks/custom-content/useCustomContent' |
| 11 | import { buildStudioPageTitle } from '@/lib/page-title' |
| 12 | import type { NextPageWithLayout } from '@/types' |
| 13 | |
| 14 | /** |
| 15 | * No org selected yet, create a new one |
| 16 | */ |
| 17 | const 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 | |
| 113 | Wizard.getLayout = (page) => ( |
| 114 | <AppLayout> |
| 115 | <DefaultLayout hideMobileMenu headerTitle="New organization"> |
| 116 | <WizardLayout>{page}</WizardLayout> |
| 117 | </DefaultLayout> |
| 118 | </AppLayout> |
| 119 | ) |
| 120 | |
| 121 | export default Wizard |