Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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' }),
});
Expand All @@ -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}.
</Text>
{tokenBalance > 0 ? (
<Text size="small" variant="danger">
You have {tokenBalance.toString()} tokens remaining. Deleting
the {orgLabelLower} forfeits them. Contact support to get the
amount transferred to your bank account.
</Text>
) : null}
Comment on lines +108 to +114

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not treat an unavailable token balance as zero.

useTokens initializes tokenBalance to 0n when the balance response is absent. This branch hides the warning while the confirm button remains enabled. A slow or failed balance request can therefore let a user delete an organization with a positive balance without seeing the forfeiture warning.

Expose the balance query’s loading and error state. Keep the destructive action unavailable, or require an explicit unresolved-balance confirmation, until a successful balance is known.

<Field
label={`Please type name of the ${orgLabel} to confirm.`}
error={
Expand Down Expand Up @@ -146,7 +156,7 @@ export const DeleteOrganizationDialog = ({
variant="solid"
color="danger"
type="submit"
disabled={!deleteTitle || !isAcknowledged}
disabled={!deleteTitle || !isAcknowledged || isSubmitting}
data-test-id="frontier-sdk-delete-organization-btn"
loading={isSubmitting}
loaderText="Deleting..."
Expand Down
49 changes: 44 additions & 5 deletions web/sdk/client/views/general/general-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import {
import { useQueryClient } from '@tanstack/react-query';
import {
FrontierServiceQueries,
UpdateOrganizationRequestSchema
UpdateOrganizationRequestSchema,
RQLRequestSchema,
RQLFilterSchema,
RQLSortSchema
} from '@raystack/proton/frontier';
import {
Button,
Expand All @@ -28,6 +31,9 @@ import {
import { useFrontier } from '../../contexts/FrontierContext';
import { usePermissions } from '../../hooks/usePermissions';
import { useTerminology } from '../../hooks/useTerminology';
import { useOrganizationInvoices } from '../../hooks/useOrganizationInvoices';
import { INVOICE_STATES } from '../../utils/constants';
import { DEFAULT_PAGE_SIZE } from '../../utils/connect-pagination';
import { PERMISSIONS, shouldShowComponent } from '../../../utils';
import { AuthTooltipMessage } from '../../utils';
import { ViewContainer } from '../../components/view-container';
Expand All @@ -47,6 +53,26 @@ const generalSchema = yup

type FormData = yup.InferType<typeof generalSchema>;

// 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;
Expand Down Expand Up @@ -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
);
Comment on lines +126 to +132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Fail closed when the invoice query is not verified.

useOrganizationInvoices exposes isLoading and isError, but this code reads only invoices. During the initial request or after a failed request, invoices can be empty and hasUnpaidInvoices becomes false. GeneralView can then enable deletion without proving that no blocking invoice exists.

Use the query status in the delete gate. Keep the button disabled while the check is loading or failed. Show an explicit loading or verification-error tooltip.

Based on learnings: count-dependent actions should remain unavailable when the query fails because the UI cannot show a reliable count.

Also applies to: 317-325, 332-337

Source: Learnings


// Update organization form
const { mutateAsync: updateOrganization } = useMutation(
FrontierServiceQueries.updateOrganization,
Expand Down Expand Up @@ -280,22 +314,27 @@ export function GeneralView({ onDeleteSuccess, urlPrefix }: GeneralViewProps = {
</Text>
<Tooltip>
<Tooltip.Trigger
disabled={canDeleteWorkspace}
disabled={canDeleteWorkspace && !hasUnpaidInvoices}
render={<span className={styles.fitContent} />}
>
<Button
variant="solid"
color="danger"
onClick={() => setShowDeleteDialog(true)}
disabled={!canDeleteWorkspace}
disabled={!canDeleteWorkspace || hasUnpaidInvoices}
data-test-id="frontier-sdk-delete-organization-btn"
>
Delete {orgLabelLower}
</Button>
</Tooltip.Trigger>
{!canDeleteWorkspace && (
{!canDeleteWorkspace ? (
<Tooltip.Content>{AuthTooltipMessage}</Tooltip.Content>
)}
) : hasUnpaidInvoices ? (
<Tooltip.Content>
There are unpaid invoices. Pay them from the billing page
before deleting the {orgLabelLower}.
</Tooltip.Content>
) : null}
</Tooltip>
</>
)}
Expand Down
Loading