mirror of
https://github.com/supabase/supabase.git
synced 2026-06-16 02:26:42 +08:00
## 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 --> [](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 -->
146 lines
4.4 KiB
TypeScript
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
|