Files
supabase/apps/studio/components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx
Gildas Garcia e26303cf9c chore: migrate billing Modal to Dialog (#46385)
## Problem

We still use the deprecated `Modal` for:
- Adding a new payment card
- Deleting a payment a card
- Changing the payment method
- Displaying the spend cap details when creating a new org

## Solution

- use `Dialog` instead

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Updated billing dialogs (add/change/delete payment methods and spend
cap) to use a newer dialog/alert dialog system.
* Result: more consistent dialog behavior, clearer confirmation flows,
and improved handling of loading/confirmation states for payment
actions.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46385?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-27 13:51:41 +02:00

146 lines
4.4 KiB
TypeScript

import HCaptcha from '@hcaptcha/react-hcaptcha'
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import { useTheme } from 'next-themes'
import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Dialog, DialogContent, DialogHeader, DialogSectionSeparator, DialogTitle } from 'ui'
import AddPaymentMethodForm from './AddPaymentMethodForm'
import { getStripeElementsAppearanceOptions } from './Payment.utils'
import { useOrganizationPaymentMethodSetupIntent } from '@/data/organizations/organization-payment-method-setup-intent-mutation'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { STRIPE_PUBLIC_KEY } from '@/lib/constants'
interface AddNewPaymentMethodModalProps {
visible: boolean
returnUrl: string
onCancel: () => void
onConfirm: () => void
}
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY)
const AddNewPaymentMethodModal = ({
visible,
returnUrl,
onCancel,
onConfirm,
}: AddNewPaymentMethodModalProps) => {
const { resolvedTheme } = useTheme()
const [intent, setIntent] = useState<any>()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
const [captchaRef, setCaptchaRef] = useState<HCaptcha | null>(null)
const { mutate: setupIntent } = useOrganizationPaymentMethodSetupIntent({
onSuccess: (intent) => {
setIntent(intent)
},
onError: (error) => {
toast.error(`Failed to setup intent: ${error.message}`)
},
})
const captchaRefCallback = useCallback((node: any) => {
setCaptchaRef(node)
}, [])
useEffect(() => {
const initSetupIntent = async (hcaptchaToken: string | undefined) => {
const slug = selectedOrganization?.slug
if (!slug) return console.error('Slug is required')
if (!hcaptchaToken) return console.error('HCaptcha token required')
setIntent(undefined)
setupIntent({ slug, hcaptchaToken })
}
const loadPaymentForm = async () => {
if (visible && 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()
}
}
loadPaymentForm()
}, [visible, captchaRef])
const resetCaptcha = () => {
setCaptchaToken(null)
captchaRef?.resetCaptcha()
}
const options = {
clientSecret: intent ? intent.client_secret : '',
appearance: getStripeElementsAppearanceOptions(resolvedTheme),
} as any
const onLocalCancel = () => {
setIntent(undefined)
return onCancel()
}
const onLocalConfirm = () => {
setIntent(undefined)
return onConfirm()
}
return (
// We cant display the hCaptcha in the modal, as the modal auto-closes when clicking the captcha
// So we only show the modal if the captcha has been executed successfully (intent loaded)
<>
<HCaptcha
ref={captchaRefCallback}
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
size="invisible"
onOpen={() => {
// [Joshen] This is to ensure that hCaptcha popup remains clickable
if (document !== undefined) document.body.classList.add('pointer-events-auto!')
}}
onClose={() => {
onLocalCancel()
if (document !== undefined) document.body.classList.remove('pointer-events-auto!')
}}
onVerify={(token) => {
setCaptchaToken(token)
if (document !== undefined) document.body.classList.remove('pointer-events-auto!')
}}
onExpire={() => {
setCaptchaToken(null)
}}
/>
<Dialog open={visible && intent !== undefined} onOpenChange={onLocalCancel}>
<DialogContent size="medium" className="PAYMENT">
<DialogHeader>
<DialogTitle>Add new payment method</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<Elements stripe={stripePromise} options={options}>
<AddPaymentMethodForm
returnUrl={returnUrl}
onCancel={onLocalCancel}
onConfirm={onLocalConfirm}
/>
</Elements>
</DialogContent>
</Dialog>
</>
)
}
export default AddNewPaymentMethodModal