Files
supabase/apps/studio/pages/new/index.tsx
T
Kevin Grüneberg 740eeb8504 feat: mandatory address input for paying customers (#37337)
- Mandatory address input when adding a new payment method
- Removed the global HCaptcha store that wasn't used consistently and would sometimes block payment method changes
- Remove the custom billing address data & tax id form on org creation and plan upgrades in favour of Stripe's `AddressElement`
- Unify usage of the Stripe payment elements into a single component
- Customers can mark "Purchasing as a business" and will then be able to put down a tax id
- Adjusted billing address form to have better labels + tax id is filtered down to selected country
- Adjusted Stripe Elements styling to use floating labels (otherwise very hard to use with address element) + additional styling changes
- New flag to filter out payment methods that do not have an address for org upgrades and credit top ups, this will be enforced a few days after rolling this out
- Added Google Maps Places API integration for address auto-completion via Stripe AddressElement
- Upgraded Stripe dependencies
- Slight adjustments to styling of plan upgrade modal
2025-07-23 18:16:10 +08:00

109 lines
2.9 KiB
TypeScript

import HCaptcha from '@hcaptcha/react-hcaptcha'
import { useCallback, useEffect, useState } from 'react'
import { NewOrgForm } from 'components/interfaces/Organization'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import WizardLayout from 'components/layouts/WizardLayout'
import { SetupIntentResponse, useSetupIntent } from 'data/stripe/setup-intent-mutation'
import type { NextPageWithLayout } from 'types'
/**
* No org selected yet, create a new one
*/
const Wizard: NextPageWithLayout = () => {
const [intent, setIntent] = useState<SetupIntentResponse>()
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
const [selectedPlan, setSelectedPlan] = useState<string | null>(null)
const { mutate: setupIntent } = useSetupIntent({ onSuccess: (res) => setIntent(res) })
const captchaRefCallback = useCallback((node: any) => {
setCaptchaRef(node)
}, [])
const initSetupIntent = async (hcaptchaToken: string | undefined) => {
if (!hcaptchaToken) return console.error('Hcaptcha token is required')
// Force a reload of Elements, necessary for Stripe
// Also mitigates card testing to some extent as we generate a new captcha token
setIntent(undefined)
setupIntent({ hcaptchaToken })
}
const loadPaymentForm = async (force = false) => {
if (selectedPlan == null || selectedPlan === 'FREE') return
if (intent != null && !force) return
if (captchaRef) {
let token = captchaToken
try {
if (!token) {
const captchaResponse = await captchaRef.execute({ async: true })
token = captchaResponse?.response ?? null
}
} catch (error) {
return
}
await initSetupIntent(token ?? undefined)
resetCaptcha()
}
}
useEffect(() => {
loadPaymentForm()
}, [captchaRef, selectedPlan])
const resetSetupIntent = () => {
setIntent(undefined)
return loadPaymentForm(true)
}
const onLocalCancel = () => {
setIntent(undefined)
}
const resetCaptcha = () => {
setCaptchaToken(null)
captchaRef?.resetCaptcha()
}
return (
<>
<HCaptcha
ref={captchaRefCallback}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onVerify={(token) => {
setCaptchaToken(token)
}}
onClose={onLocalCancel}
onExpire={() => {
setCaptchaToken(null)
}}
/>
<NewOrgForm
setupIntent={intent}
onPaymentMethodReset={() => resetSetupIntent()}
onPlanSelected={(plan) => setSelectedPlan(plan)}
/>
</>
)
}
Wizard.getLayout = (page) => (
<AppLayout>
<DefaultLayout headerTitle="New organization">
<WizardLayout>{page}</WizardLayout>
</DefaultLayout>
</AppLayout>
)
export default Wizard