From 347b39fe8fbfa19b0c4876a84885e5c15e85104a Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Thu, 13 Aug 2026 17:03:27 +0530 Subject: [PATCH] feat(sdk): guard the delete organization flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The delete button greys out while the org has open invoices with a non-zero amount, and its hover tooltip says to pay them from the billing page first — the server refuses the delete in that state anyway, this stops the user before the failed call. The delete dialog warns when tokens remain on the billing account: deleting forfeits them, and support can transfer the amount to the user's bank account. Confirming the dialog is the user's consent. The confirm button also disables while the request is in flight, so repeated clicks cannot fire the delete twice, and a failed_precondition response shows the server's reasons instead of a generic error. --- .../components/delete-organization-dialog.tsx | 12 ++++- web/sdk/client/views/general/general-view.tsx | 49 +++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/web/sdk/client/views/general/components/delete-organization-dialog.tsx b/web/sdk/client/views/general/components/delete-organization-dialog.tsx index 52f9bffddc..d01131dc61 100644 --- a/web/sdk/client/views/general/components/delete-organization-dialog.tsx +++ b/web/sdk/client/views/general/components/delete-organization-dialog.tsx @@ -20,6 +20,7 @@ import { } from '@raystack/apsara'; import { useFrontier } from '../../../contexts/FrontierContext'; import { useTerminology } from '../../../hooks/useTerminology'; +import { useTokens } from '../../../hooks/useTokens'; import { handleConnectError } from '~/utils/error'; const deleteOrgSchema = yup @@ -44,6 +45,7 @@ export const DeleteOrganizationDialog = ({ const orgLabel = t.organization({ case: 'capital' }); const orgLabelLower = t.organization({ case: 'lower' }); const [isAcknowledged, setIsAcknowledged] = useState(false); + const { tokenBalance } = useTokens(); const { mutateAsync: deleteOrganization } = useMutation( FrontierServiceQueries.deleteOrganization @@ -83,6 +85,7 @@ export const DeleteOrganizationDialog = ({ } catch (error) { handleConnectError(error, { PermissionDenied: () => toastManager.add({ title: "You don't have permission to perform this action", type: 'error' }), + FailedPrecondition: (err) => toastManager.add({ title: `Cannot delete this ${orgLabelLower} yet`, description: err.rawMessage, type: 'error' }), NotFound: (err) => toastManager.add({ title: 'Not found', description: err.message, type: 'error' }), Default: (err) => toastManager.add({ title: 'Something went wrong', description: err.message, type: 'error' }), }); @@ -102,6 +105,13 @@ export const DeleteOrganizationDialog = ({ This action can not be undone. This will permanently delete all the projects and resources in {organization?.title}. + {tokenBalance > 0 ? ( + + You have {tokenBalance.toString()} tokens remaining. Deleting + the {orgLabelLower} forfeits them. Contact support to get the + amount transferred to your bank account. + + ) : null} ; +// Open invoices with a non-zero amount. The server refuses the delete while +// any exist, so the delete button greys out and explains why. +const OPEN_INVOICES_QUERY = create(RQLRequestSchema, { + filters: [ + create(RQLFilterSchema, { + name: 'state', + operator: 'eq', + value: { case: 'stringValue', value: INVOICE_STATES.OPEN } + }), + create(RQLFilterSchema, { + name: 'amount', + operator: 'gt', + value: { case: 'numberValue', value: 0 } + }) + ], + sort: [create(RQLSortSchema, { name: 'created_at', order: 'desc' })], + offset: 0, + limit: DEFAULT_PAGE_SIZE +}); + export interface GeneralViewProps { onDeleteSuccess?: () => void; urlPrefix?: string; @@ -97,6 +123,14 @@ export function GeneralView({ onDeleteSuccess, urlPrefix }: GeneralViewProps = { const isLoading = !organization?.id || isActiveOrganizationLoading || isPermissionsFetching; + const { invoices } = useOrganizationInvoices({ + query: OPEN_INVOICES_QUERY, + enabled: canDeleteWorkspace && !!organization?.id + }); + const hasUnpaidInvoices = invoices.some( + inv => inv.state === INVOICE_STATES.OPEN + ); + // Update organization form const { mutateAsync: updateOrganization } = useMutation( FrontierServiceQueries.updateOrganization, @@ -280,22 +314,27 @@ export function GeneralView({ onDeleteSuccess, urlPrefix }: GeneralViewProps = { } > - {!canDeleteWorkspace && ( + {!canDeleteWorkspace ? ( {AuthTooltipMessage} - )} + ) : hasUnpaidInvoices ? ( + + There are unpaid invoices. Pay them from the billing page + before deleting the {orgLabelLower}. + + ) : null} )}