Merge branch 'master' into aliwaseem/fe-2819-remove-the-old-filter-bar-completely

This commit is contained in:
Ali Waseem authored and GitHub committed 2026-04-14 11:09:36 -06:00
commit f09081b138
7 files changed
+217 -126

No files matched your search

@@ -1,5 +1,4 @@
import { useQueryClient } from '@tanstack/react-query'
import { isEqual } from 'lodash'
import { useRef, useState } from 'react'
import { toast } from 'sonner'
import { Button, Checkbox_Shadcn_, Label_Shadcn_, Modal } from 'ui'
@@ -14,7 +13,6 @@ import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organi
import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation'
import { useOrganizationPaymentMethodMarkAsDefaultMutation } from '@/data/organizations/organization-payment-method-default-mutation'
import { useOrganizationTaxIdQuery } from '@/data/organizations/organization-tax-id-query'
import { useOrganizationTaxIdUpdateMutation } from '@/data/organizations/organization-tax-id-update-mutation'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
interface AddPaymentMethodFormProps {
@@ -41,9 +39,14 @@ const AddPaymentMethodForm = ({ onCancel, onConfirm }: AddPaymentMethodFormProps
const queryClient = useQueryClient()
const { mutateAsync: markAsDefault } = useOrganizationPaymentMethodMarkAsDefaultMutation()
const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation()
const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation()
const { data: taxId, isPending: isCustomerTaxIdLoading } = useOrganizationTaxIdQuery({
const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({
onError: () => {},
})
const {
data: taxId,
isPending: isCustomerTaxIdLoading,
isError: isTaxIdError,
} = useOrganizationTaxIdQuery({
slug: selectedOrganization?.slug,
})
@@ -59,11 +62,70 @@ const AddPaymentMethodForm = ({ onCancel, onConfirm }: AddPaymentMethodFormProps
document.body.classList.add('!pointer-events-auto')
}
if (isPrimaryBillingAddress && isTaxIdError) {
toast.error('Unable to load current tax ID. Please try again.')
setIsSaving(false)
if (document !== undefined) {
document.body.classList.remove('!pointer-events-auto')
}
return
}
// Validate address/tax ID with a dry run before proceeding with Stripe,
// so validation errors (e.g. invalid tax ID) block the flow early.
const formValues = isPrimaryBillingAddress
? await paymentRef.current?.getFormValues()
: undefined
if (isPrimaryBillingAddress && !formValues) {
setIsSaving(false)
if (document !== undefined) {
document.body.classList.remove('!pointer-events-auto')
}
return
}
if (isPrimaryBillingAddress && formValues) {
try {
await updateCustomerProfile({
slug: selectedOrganization?.slug,
address: formValues.address,
billing_name: formValues.customerName,
tax_id: formValues.taxId,
dry_run: true,
})
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to validate billing profile')
setIsSaving(false)
if (document !== undefined) {
document.body.classList.remove('!pointer-events-auto')
}
return
}
}
// Dry run passed — proceed with Stripe payment method creation / 3DS
const result = await paymentRef.current?.confirmSetup()
if (!result) {
setIsSaving(false)
} else {
// Stripe succeeded — persist the customer profile update for real
if (isPrimaryBillingAddress && formValues) {
try {
await updateCustomerProfile({
slug: selectedOrganization?.slug,
address: formValues.address,
billing_name: formValues.customerName,
tax_id: formValues.taxId,
})
} catch {
toast.error(
'Your payment method was added successfully, but we could not save your billing address. Please update it in your organization settings.'
)
}
}
if (
isDefaultPaymentMethod &&
selectedOrganization &&
@@ -104,28 +166,6 @@ const AddPaymentMethodForm = ({ onCancel, onConfirm }: AddPaymentMethodFormProps
}
}
if (isPrimaryBillingAddress) {
try {
if (
result.address &&
(!isEqual(result.address, customerProfile?.address) ||
customerProfile?.billing_name !== result.customerName)
) {
await updateCustomerProfile({
slug: selectedOrganization?.slug,
billing_name: result.customerName,
address: result.address,
})
}
if (result.taxId && !isEqual(result.taxId, taxId)) {
await updateTaxId({ taxId: result.taxId, slug: selectedOrganization?.slug })
}
} catch (error) {
toast.error('Failed to update billing address')
}
}
setIsSaving(false)
onConfirm()
}
@@ -75,6 +75,14 @@ export type PaymentMethodElementRef = {
}
| undefined
>
getFormValues: () => Promise<
| {
address: CustomerAddress
customerName: string
taxId: CustomerTaxId | null
}
| undefined
>
}
export const NewPaymentMethodElement = forwardRef(
@@ -162,9 +170,13 @@ export const NewPaymentMethodElement = forwardRef(
PaymentMethodElementRef['createPaymentMethod']
> => {
if (!stripe || !elements) return
await form.trigger()
const isValid = await form.trigger()
if (purchasingAsBusiness && availableTaxIds.length > 0 && !form.getValues('tax_id_value')) {
if (
purchasingAsBusiness &&
availableTaxIds.length > 0 &&
(!isValid || !form.getValues('tax_id_value'))
) {
return
}
@@ -192,7 +204,8 @@ export const NewPaymentMethodElement = forwardRef(
}
function getConfiguredTaxId(): CustomerTaxId | null {
return purchasingAsBusiness && selectedTaxId
const isValidForCountry = selectedTaxId && availableTaxIds.includes(selectedTaxId)
return purchasingAsBusiness && isValidForCountry
? {
country: getEffectiveTaxCountry(selectedTaxId),
type: selectedTaxId.type,
@@ -229,9 +242,37 @@ export const NewPaymentMethodElement = forwardRef(
}
}
const getFormValues = async (): ReturnType<PaymentMethodElementRef['getFormValues']> => {
if (!elements) return
const isValid = await form.trigger()
if (
purchasingAsBusiness &&
availableTaxIds.length > 0 &&
(!isValid || !form.getValues('tax_id_value'))
) {
return
}
const { error: submitError } = await elements.submit()
if (submitError) return
const addressElement = await elements.getElement('address')!.getValue()
return {
address: {
...addressElement.value.address,
line2: addressElement.value.address.line2 || undefined,
},
customerName: addressElement.value.name,
taxId: getConfiguredTaxId(),
}
}
useImperativeHandle(ref, () => ({
createPaymentMethod,
confirmSetup,
getFormValues,
}))
const addressOptions: StripeAddressElementOptions = useMemo(
@@ -254,10 +295,11 @@ export const NewPaymentMethodElement = forwardRef(
[purchasingAsBusiness]
)
// Preselect tax id when the country changes (if there are available tax ids for that country)
// Reset tax ID fields when the billing country changes and preselect the
// first available tax ID for the new country.
const prevCountryRef = useRef(addressCountry)
useEffect(() => {
if (!availableTaxIds.length || !addressCountry) return
if (!addressCountry) return
const isCountryChange =
prevCountryRef.current !== undefined && prevCountryRef.current !== addressCountry
@@ -266,12 +308,18 @@ export const NewPaymentMethodElement = forwardRef(
// On country change: always reset to the new country's default
// On initial load: only preselect if there's no existing tax id
if (isCountryChange || !currentTaxId) {
const taxIdOption = availableTaxIds[0]
form.setValue('tax_id_type', taxIdOption.type)
form.setValue('tax_id_value', '')
form.setValue('tax_id_name', taxIdOption.name)
if (availableTaxIds.length) {
const taxIdOption = availableTaxIds[0]
form.setValue('tax_id_type', taxIdOption.type)
form.setValue('tax_id_value', '')
form.setValue('tax_id_name', taxIdOption.name)
} else {
form.setValue('tax_id_type', '')
form.setValue('tax_id_value', '')
form.setValue('tax_id_name', '')
}
}
}, [availableTaxIds, addressCountry, currentTaxId])
}, [availableTaxIds, addressCountry, currentTaxId, form])
return (
<div className="space-y-2">
@@ -62,6 +62,7 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => {
const queryClient = useQueryClient()
const paymentMethodSelectionRef = useRef<{
createPaymentMethod: PaymentMethodElementRef['createPaymentMethod']
validateBillingProfile: () => Promise<boolean>
}>(null)
const { can: canTopUpCredits, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
@@ -129,6 +130,9 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => {
const token = await initHcaptcha()
const isValid = await paymentMethodSelectionRef.current?.validateBillingProfile()
if (!isValid) return
const paymentMethodResult = await paymentMethodSelectionRef.current?.createPaymentMethod()
if (!paymentMethodResult) {
return
@@ -23,6 +23,7 @@ import {
type PaymentMethodElementRef,
} from '@/components/interfaces/Billing/Payment/PaymentMethods/NewPaymentMethodElement'
import { useOrganizationCustomerProfileQuery } from '@/data/organizations/organization-customer-profile-query'
import { useOrganizationCustomerProfileUpdateMutation } from '@/data/organizations/organization-customer-profile-update-mutation'
import { useOrganizationPaymentMethodSetupIntent } from '@/data/organizations/organization-payment-method-setup-intent-mutation'
import { useOrganizationPaymentMethodsQuery } from '@/data/organizations/organization-payment-methods-query'
import { useOrganizationTaxIdQuery } from '@/data/organizations/organization-tax-id-query'
@@ -69,7 +70,14 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection(
useOrganizationCustomerProfileQuery({
slug,
})
const { data: taxId, isPending: isCustomerTaxIdLoading } = useOrganizationTaxIdQuery({ slug })
const {
data: taxId,
isPending: isCustomerTaxIdLoading,
isError: isTaxIdError,
} = useOrganizationTaxIdQuery({ slug })
const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({
onError: () => {},
})
const { data: allPaymentMethods, isPending: isLoading } = useOrganizationPaymentMethodsQuery({
slug,
@@ -176,6 +184,51 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection(
}
}, [selectedPaymentMethod, paymentMethods, onSelectPaymentMethod])
const getFormValues = async (): ReturnType<PaymentMethodElementRef['getFormValues']> => {
if (setupNewPaymentMethod || (paymentMethods?.data && paymentMethods.data.length === 0)) {
return paymentRef.current?.getFormValues()
} else {
return {
address: customerProfile?.address ?? ({} as CustomerAddress),
customerName: customerProfile?.billing_name || '',
taxId: taxId ?? null,
}
}
}
// Validate address/tax ID with a dry run before proceeding with Stripe,
// so validation errors (e.g. invalid tax ID) block the flow early.
const validateBillingProfile = async (): Promise<boolean> => {
if (!useAsDefaultBillingAddress) return true
if (isTaxIdError || isCustomerTaxIdLoading) {
toast.error(
isTaxIdError
? 'Unable to load current tax ID. Please try again.'
: 'Tax ID is still loading. Please wait and try again.'
)
return false
}
const formValues = await getFormValues()
if (!formValues) return false
try {
await updateCustomerProfile({
slug,
address: formValues.address,
billing_name: formValues.customerName,
tax_id: formValues.taxId,
dry_run: true,
})
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to validate billing profile')
return false
}
return true
}
// If createPaymentMethod already exists, use it. Otherwise, define it here.
const createPaymentMethod = async (): ReturnType<
PaymentMethodElementRef['createPaymentMethod']
@@ -203,6 +256,7 @@ const PaymentMethodSelection = forwardRef(function PaymentMethodSelection(
useImperativeHandle(ref, () => ({
createPaymentMethod,
validateBillingProfile,
}))
return (
@@ -101,6 +101,7 @@ export const SubscriptionPlanUpdateDialog = ({
const [paymentConfirmationLoading, setPaymentConfirmationLoading] = useState(false)
const paymentMethodSelectionRef = useRef<{
createPaymentMethod: PaymentMethodElementRef['createPaymentMethod']
validateBillingProfile: () => Promise<boolean>
}>(null)
const billingViaPartner = subscription?.billing_via_partner === true
@@ -180,6 +181,14 @@ export const SubscriptionPlanUpdateDialog = ({
setPaymentConfirmationLoading(true)
if (paymentMethodSelectionRef.current) {
const isValid = await paymentMethodSelectionRef.current.validateBillingProfile()
if (!isValid) {
setPaymentConfirmationLoading(false)
return
}
}
const result = await paymentMethodSelectionRef.current?.createPaymentMethod()
if (result) {
setSelectedPaymentMethod(result.paymentMethod.id)
@@ -12,6 +12,8 @@ export type OrganizationCustomerProfileUpdateVariables = {
billing_name: string
/** Pass a tax ID object to set/update, `null` to clear, or `undefined` to leave unchanged */
tax_id?: CustomerTaxId | null
/** When true, validates the request without persisting changes */
dry_run?: boolean
}
export async function updateOrganizationCustomerProfile({
@@ -19,6 +21,7 @@ export async function updateOrganizationCustomerProfile({
address,
billing_name,
tax_id,
dry_run,
}: OrganizationCustomerProfileUpdateVariables) {
if (!slug) return console.error('Slug is required')
@@ -36,6 +39,7 @@ export async function updateOrganizationCustomerProfile({
: tax_id !== undefined
? { tax_id }
: {}),
...(dry_run ? { dry_run } : {}),
},
})
if (error) throw handleError(error)
@@ -67,9 +71,14 @@ export const useOrganizationCustomerProfileUpdateMutation = ({
>({
mutationFn: (vars) => updateOrganizationCustomerProfile(vars),
async onSuccess(data, variables, context) {
const { address, slug, billing_name, tax_id } = variables
const { address, slug, billing_name, tax_id, dry_run } = variables
// We do not invalidate here as GET endpoint data is stale for 1-2 seconds, so we handle state manually
if (dry_run) {
await onSuccess?.(data, variables, context)
return
}
// Optimistically update the cache for immediate UI consistency
queryClient.setQueriesData(
{ queryKey: organizationKeys.customerProfile(slug) },
(prev: any) => {
@@ -82,11 +91,23 @@ export const useOrganizationCustomerProfileUpdateMutation = ({
}
)
// Update tax ID cache if tax_id was part of this update
if (tax_id !== undefined) {
queryClient.setQueryData(organizationKeys.taxId(slug), tax_id)
}
// Refetch after a delay to pick up server-canonical values (e.g. normalized tax IDs).
// The GET endpoint can be stale for 1-2 seconds after an update.
setTimeout(() => {
queryClient.invalidateQueries({
queryKey: organizationKeys.customerProfile(slug),
})
if (tax_id !== undefined) {
queryClient.invalidateQueries({
queryKey: organizationKeys.taxId(slug),
})
}
}, 3000)
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
@@ -1,85 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { organizationKeys } from './keys'
import { invalidateOrganizationsQuery } from './organizations-query'
import { del, handleError, put } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type OrganizationTaxIdUpdateVariables = {
slug?: string
taxId: { type: string; value: string; country?: string } | null
}
export async function updateOrganizationTaxId({ slug, taxId }: OrganizationTaxIdUpdateVariables) {
if (!slug) {
throw new Error('Slug is required')
}
if (taxId != null) {
const { data, error } = await put(`/platform/organizations/{slug}/tax-ids`, {
params: {
path: {
slug,
},
},
body: { type: taxId.type, value: taxId.value, country: taxId.country },
})
if (error) handleError(error)
return data
} else {
// @ts-ignore wrong type due to API versioning
const { data, error } = await del(`/platform/organizations/{slug}/tax-ids`, {
params: {
path: {
slug,
},
},
headers: {
Version: '2',
},
})
if (error) handleError(error)
return data
}
}
type OrganizationTaxIdUpdateData = Awaited<ReturnType<typeof updateOrganizationTaxId>>
export const useOrganizationTaxIdUpdateMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<
OrganizationTaxIdUpdateData,
ResponseError,
OrganizationTaxIdUpdateVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<OrganizationTaxIdUpdateData, ResponseError, OrganizationTaxIdUpdateVariables>({
mutationFn: (vars) => updateOrganizationTaxId(vars),
async onSuccess(data, variables, context) {
const { slug } = variables
// We already have the data, no need to refetch
queryClient.setQueryData(organizationKeys.taxId(slug), data.tax_id)
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to update tax id: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}