mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix: 3ds confirmation / downgrades (#36600)
- Remove captcha for downgrades to Free, it is not needed - Properly handle 3DS if customer has multiple payment methods - Move payment method invalidation to react-query
This commit is contained in:
1 parent
c8dbc57051
commit
680af04a06
7 files changed
+26
-66
No files matched your search
@@ -7,11 +7,9 @@ export const PaymentConfirmation = ({
|
||||
paymentIntentSecret,
|
||||
onPaymentIntentConfirm,
|
||||
onLoadingChange,
|
||||
paymentMethodId,
|
||||
onError,
|
||||
}: {
|
||||
paymentIntentSecret: string
|
||||
paymentMethodId: string
|
||||
onPaymentIntentConfirm: (response: PaymentIntentResult) => void
|
||||
onLoadingChange: (loading: boolean) => void
|
||||
onError?: (error: Error) => void
|
||||
@@ -22,7 +20,7 @@ export const PaymentConfirmation = ({
|
||||
if (stripe && paymentIntentSecret) {
|
||||
onLoadingChange(true)
|
||||
stripe!
|
||||
.confirmCardPayment(paymentIntentSecret, { payment_method: paymentMethodId })
|
||||
.confirmCardPayment(paymentIntentSecret)
|
||||
.then((res) => {
|
||||
onPaymentIntentConfirm(res)
|
||||
onLoadingChange(false)
|
||||
|
||||
@@ -325,7 +325,6 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => {
|
||||
paymentIntentConfirmed(paymentIntentConfirmation)
|
||||
}
|
||||
onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)}
|
||||
paymentMethodId={form.getValues().paymentMethod}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
|
||||
+2
-39
@@ -1,12 +1,10 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { includes, without } from 'lodash'
|
||||
import { useReducer, useRef, useState } from 'react'
|
||||
import { useReducer, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
|
||||
import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation'
|
||||
import type { OrgSubscription } from 'data/subscriptions/types'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { Alert, Button, Input, Modal } from 'ui'
|
||||
import type { ProjectInfo } from '../../../../../data/projects/projects-query'
|
||||
@@ -22,17 +20,14 @@ export interface ExitSurveyModalProps {
|
||||
// [Joshen] For context - Exit survey is only when going to Free Plan from a paid plan
|
||||
const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) => {
|
||||
const { slug } = useParams()
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
|
||||
const [message, setMessage] = useState('')
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const [selectedReasons, dispatchSelectedReasons] = useReducer(reducer, [])
|
||||
|
||||
const subscriptionUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const { mutate: updateOrgSubscription, isLoading: isUpdating } = useOrgSubscriptionUpdateMutation(
|
||||
{
|
||||
onError: (error) => {
|
||||
resetCaptcha()
|
||||
toast.error(`Failed to downgrade project: ${error.message}`)
|
||||
},
|
||||
}
|
||||
@@ -55,23 +50,12 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
|
||||
}
|
||||
}
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setCaptchaToken(null)
|
||||
captchaRef.current?.resetCaptcha()
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (selectedReasons.length === 0) {
|
||||
return toast.error('Please select at least one reason for canceling your subscription')
|
||||
}
|
||||
|
||||
let token = captchaToken
|
||||
|
||||
if (!token) {
|
||||
const captchaResponse = await captchaRef.current?.execute({ async: true })
|
||||
token = captchaResponse?.response ?? null
|
||||
await downgradeOrganization()
|
||||
}
|
||||
await downgradeOrganization()
|
||||
}
|
||||
|
||||
const downgradeOrganization = async () => {
|
||||
@@ -83,7 +67,6 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
|
||||
{ slug, tier: 'tier_free' },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
resetCaptcha()
|
||||
try {
|
||||
await sendExitSurvey({
|
||||
orgSlug: slug,
|
||||
@@ -110,26 +93,6 @@ const ExitSurveyModal = ({ visible, projects, onClose }: ExitSurveyModalProps) =
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="self-center">
|
||||
<HCaptcha
|
||||
ref={captchaRef}
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
|
||||
size="invisible"
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token)
|
||||
if (document !== undefined) document.body.classList.remove('!pointer-events-auto')
|
||||
}}
|
||||
onExpire={() => setCaptchaToken(null)}
|
||||
onOpen={() => {
|
||||
// [Joshen] This is to ensure that hCaptcha popup remains clickable
|
||||
if (document !== undefined) document.body.classList.add('!pointer-events-auto')
|
||||
}}
|
||||
onClose={() => {
|
||||
if (document !== undefined) document.body.classList.remove('!pointer-events-auto')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
hideFooter
|
||||
size="xlarge"
|
||||
|
||||
+7
-22
@@ -85,7 +85,6 @@ export const SubscriptionPlanUpdateDialog = ({
|
||||
projects,
|
||||
}: Props) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const queryClient = useQueryClient()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>()
|
||||
const [paymentIntentSecret, setPaymentIntentSecret] = useState<string | null>(null)
|
||||
@@ -182,23 +181,6 @@ export const SubscriptionPlanUpdateDialog = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (paymentMethod) {
|
||||
queryClient.setQueriesData(
|
||||
organizationKeys.paymentMethods(selectedOrganization.slug),
|
||||
(prev: any) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
defaultPaymentMethodId: paymentMethod?.id,
|
||||
data: prev.data.map((pm: any) => ({
|
||||
...pm,
|
||||
is_default: pm.id === paymentMethod?.id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// If the user is downgrading from team, should have spend cap disabled by default
|
||||
const tier =
|
||||
subscription?.plan?.id === 'team' && selectedTier === PRICING_TIER_PRODUCT_IDS.PRO
|
||||
@@ -253,6 +235,10 @@ export const SubscriptionPlanUpdateDialog = ({
|
||||
<Dialog
|
||||
open={selectedTier !== undefined && selectedTier !== 'tier_free'}
|
||||
onOpenChange={(open) => {
|
||||
// Do not allow closing mid-change
|
||||
if (isUpdating || paymentConfirmationLoading || isConfirming) {
|
||||
return
|
||||
}
|
||||
if (!open) onClose()
|
||||
}}
|
||||
>
|
||||
@@ -561,16 +547,16 @@ export const SubscriptionPlanUpdateDialog = ({
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
{!billingViaPartner && !subscriptionPreviewIsLoading && changeType === 'upgrade' && (
|
||||
{!billingViaPartner && subscriptionPreview != null && changeType === 'upgrade' && (
|
||||
<div className="space-y-2 mb-4">
|
||||
<BillingCustomerDataExistingOrgDialog />
|
||||
|
||||
<PaymentMethodSelection
|
||||
ref={paymentMethodSelection}
|
||||
selectedPaymentMethod={selectedPaymentMethod}
|
||||
onSelectPaymentMethod={setSelectedPaymentMethod}
|
||||
onSelectPaymentMethod={() => {}}
|
||||
createPaymentMethodInline={
|
||||
subscriptionPreview?.pending_subscription_flow === true
|
||||
subscriptionPreview.pending_subscription_flow === true
|
||||
}
|
||||
readOnly={paymentConfirmationLoading || isConfirming || isUpdating}
|
||||
/>
|
||||
@@ -700,7 +686,6 @@ export const SubscriptionPlanUpdateDialog = ({
|
||||
paymentIntentConfirmed(paymentIntentConfirmation)
|
||||
}
|
||||
onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)}
|
||||
paymentMethodId={selectedPaymentMethod!}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
|
||||
@@ -645,7 +645,6 @@ const NewOrgForm = ({ onPaymentMethodReset, setupIntent, onPlanSelected }: NewOr
|
||||
paymentIntentConfirmed(paymentIntentConfirmation)
|
||||
}
|
||||
onLoadingChange={(loading) => setPaymentConfirmationLoading(loading)}
|
||||
paymentMethodId={paymentMethod.id}
|
||||
onError={(err) => {
|
||||
toast.error(err.message, { duration: 10_000 })
|
||||
setNewOrgLoading(false)
|
||||
|
||||
@@ -74,7 +74,9 @@ export const useConfirmPendingSubscriptionChangeMutation = ({
|
||||
queryClient.invalidateQueries(invoicesKeys.orgUpcomingPreview(slug)),
|
||||
queryClient.invalidateQueries(organizationKeys.detail(slug)),
|
||||
queryClient.invalidateQueries(organizationKeys.list()),
|
||||
queryClient.invalidateQueries(organizationKeys.paymentMethods(slug)),
|
||||
])
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -68,6 +68,20 @@ export const useOrgSubscriptionUpdateMutation = ({
|
||||
queryClient.invalidateQueries(organizationKeys.detail(slug)),
|
||||
queryClient.invalidateQueries(organizationKeys.list()),
|
||||
])
|
||||
|
||||
if (variables.paymentMethod) {
|
||||
queryClient.setQueriesData(organizationKeys.paymentMethods(slug), (prev: any) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
defaultPaymentMethodId: variables.paymentMethod,
|
||||
data: prev.data.map((pm: any) => ({
|
||||
...pm,
|
||||
is_default: pm.id === variables.paymentMethod,
|
||||
})),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
|
||||
Reference in new issue
Block a user