You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
DeleteUserDialog runs the whole account delete. A click runs CheckCurrentUserDelete first, then opens the matching dialog. The trigger slot gets { onClick, loading }, so the trigger button is disabled and shows a spinner, with its label unchanged, while the check runs. This matches the org and project delete buttons.
A host can also start the delete with handle.open(), using a handle from DeleteUserDialog.createHandle(). It runs the same check, so a menu item that has already closed can still start the delete. onDeleted is required: the SDK does not navigate.
When the user is the only owner of an organization with other members, the dialog asks for an ownership transfer. When an organization that would be deleted with the account is blocked, it lists the blockers for each one. Both go through the deleteBlockedDialog slot with entity: 'user'.
Otherwise it shows the Delete Account confirmation. Delete stays disabled until the user types their email and ticks the acknowledgement.
A refused delete checks again, because the refusal has no grouping by organization.
ProfileView takes enableUserDelete (off by default) and onAccountDeleted. With the flag on, the view shows a Delete Account section. A host app turns it on when it handles the delete.
Added an optional account-deletion section to the profile view, disabled by default, with a confirmation step requiring an email match and acknowledgment.
Added deletion guidance tailored to account, organization, and project blockers, including ownership-transfer requirements and grouped organization-specific details.
Added success and error notifications during the deletion process, including updated eligibility checks when deletion is blocked.
The SDK adds a user account-deletion dialog with eligibility checks, confirmation requirements, and user-specific blocker details. The profile view can optionally expose the dialog and receive a callback after deletion.
Changes: User deletion
Layer / File(s)
Summary
User deletion blocker content web/sdk/shared/types.ts, web/sdk/client/utils/delete-blockers.ts, web/sdk/client/hooks/useDeleteCheck.ts, web/sdk/client/components/delete-blocked-dialog/*
The blocked payload supports user outcomes. Conversion creates groups for organizations to delete and organizations where the user must transfer ownership. The dialog renders grouped blockers and ownership-transfer guidance.
Deletion dialog and confirmation web/sdk/client/components/delete-user-dialog/*, web/sdk/client/index.ts
The dialog checks eligibility before opening, renders blocker details or a confirmation form, and submits deletion when the normalized email matches and acknowledgment is checked. It exposes a trigger and external handle.
The profile view accepts optional deletion enablement and a completion callback. When enabled, it displays loading content or an account-deletion section with a dialog trigger.
raystack/frontier#2025: Adds SDK delete-blocker components and conversion patterns that this change extends for user deletion.
raystack/frontier#2027: Defines user deletion checks and organization outcomes converted into the blocker payload.
raystack/frontier#2031: Adds the current-user deletion check and deletion RPCs used by the dialog.
Suggested reviewers:rohilsurana
Merge Risk: Low · up to 98b1e
The deletion flow retains several usability and edge-case defects: blocker guidance can be incomplete, retries can overlap, and callback failures can produce misleading feedback. These are bounded risks suitable for explicit owner acceptance or follow-up.
Pre-merge checks: 2
Check name
Status
Explanation
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.
description: `You’re the only owner of one or more ${t.organization({ case: 'lower', plural: true })}. Before deleting your account, transfer ownership of each affected ${org} to another member.\n\nOnce ownership has been transferred, you can proceed with account deletion.`,
Show organization delete blockers in the ownership-transfer state.
When soleOwnerOf is nonempty, the user branch returns groups: []. If the same check also returns an organizationsToDelete entry with blockers, the default dialog hides them. The copy also says the user can proceed after the transfer, so the user finds the blocker only on the next attempt.
Suggested fix
--- "a/web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx"+++ "b/web/sdk/client/components/delete-blocked-dialog/delete-blocked-dialog.tsx"@@ -50,12 +50,15 @@
};
case 'user': {
const org = t.organization({ case: 'lower' });
if (context.soleOwnerOf.length > 0) {
return {
title: `Transfer ${org} ownership`,
description: `You’re the only owner of one or more ${t.organization({ case: 'lower', plural: true })}. Before deleting your account, transfer ownership of each affected ${org} to another member.\n\nOnce ownership has been transferred, you can proceed with account deletion.`,
- groups: []+ groups: context.organizationsToDelete.map(({ title, blockers }) => ({+ title,+ lines: blockerLines(blockers, messages, wording)+ }))
};
}
return {
title: 'Unable to delete account',
Also show the "you can proceed" sentence only when organizationsToDelete is empty.
Committable suggestion
IMPORTANT
Review the replacement and indentation, then test before committing.
Suggested change
if(context.soleOwnerOf.length>0){
return{
title: `Transfer ${org} ownership`,
description: `You’re the only owner of one or more ${t.organization({case: 'lower',plural: true})}. Before deleting your account, transfer ownership of each affected ${org} to another member.\n\nOnce ownership has been transferred, you can proceed with account deletion.`,
groups: []
};
if(context.soleOwnerOf.length>0){
return{
title: `Transfer ${org} ownership`,
description: `You’re the only owner of one or more ${t.organization({case: 'lower',plural: true})}. Before deleting your account, transfer ownership of each affected ${org} to another member.\n\nOnce ownership has been transferred, you can proceed with account deletion.`,
TanStack Query v5 useMutation mutateAsync called again before previous mutation resolves promise resolution behavior
Result:
Sources consulted:
- query/packages/query-core/src/mutationObserver.ts at main · TanStack/query · GitHub — https://github.com/TanStack/query/blob/main/packages/query-core/src/mutationObserver.ts
- UNPKG — https://app.unpkg.com/%40tanstack/react-query%405.59.0/files/build/modern/useMutation.cjs.map
- query/packages/react-query/src/useMutation.ts at main · TanStack/query · GitHub — https://github.com/TanStack/query/blob/main/packages/react-query/src/useMutation.ts
- tanstack-query/docs/framework/react/guides/mutations.md at main · webmasterstack/tanstack-query · GitHub — https://github.com/webmasterstack/tanstack-query/blob/main/docs/framework/react/guides/mutations.md
- query/docs/framework/react/guides/mutations.md at main · TanStack/query · GitHub — https://github.com/TanStack/query/blob/main/docs/framework/react/guides/mutations.md
- …and 25 more
Discard superseded eligibility checks in checkAndOpen.
checkAndOpen applies every completed check. Overlapping checks can finish out of order, so an older result can replace the newer dialog state. Track the latest check and ignore results from older checks, including their errors.
Suggested fix
-import { useCallback, useEffect, useState } from 'react';+import { useCallback, useEffect, useRef, useState } from 'react';
The reason will be displayed to describe this comment to others. Learn more.
Minor · Functional Correctness · Quick win
Supported by static analysis
Script executed:
rg -n -C 3 'FailedPrecondition: onDeleteRefused|checkCurrentUserDelete|DeleteCurrentUser|CheckCurrentUserDelete' web/sdk/client test --glob '*.{ts,tsx,go}'| head -170
Repository: raystack/frontier
Length of output: 11777
Script executed:
# Locate handleConnectError and its dispatch of FailedPrecondition
rg -n -F -e 'handleConnectError' -e 'FailedPrecondition' --glob '*.ts' --glob '*.tsx' web/sdk ||test"$?" -eq 1
# Server side: DeleteCurrentUser and CheckCurrentUserDelete handlers
rg -n -F -e 'DeleteCurrentUser(' -e 'CheckCurrentUserDelete(' --glob '*.go' internal cmd pkg 2>/dev/null ||test"$?" -eq 1
Repository: raystack/frontier
Length of output: 9305
Script executed:
# Read the full dispatcher
nl -ba web/sdk/utils/error.ts | sed -n '1,80p'# Read both server handlers
nl -ba internal/api/v1beta1connect/user.go | sed -n '410,470p'# Locate service-level delete and check logic
rg -n -F -e 'CheckCurrentUserDelete' -e 'func (s *Service) Delete' -e 'FailedPrecondition' -e 'ErrFailedPrecondition' --glob '*.go' core internal/api pkg 2>/dev/null ||test"$?" -eq 1
Repository: raystack/frontier
Length of output: 21904
Script executed:
# Read the handler-side blocked error mapping
nl -ba internal/api/v1beta1connect/deleter.go | sed -n '1,140p'# Locate the core deleter service and its check/delete functions
rg -n -F -e 'func (s *Service) CheckUserDelete' -e 'func (s *Service) DeleteUser' -e 'BlockedError' -e 'func (s *Service) CheckOrganizationDelete' --glob '*.go' core internal 2>/dev/null ||test"$?" -eq 1
Repository: raystack/frontier
Length of output: 9555
Script executed:
# Locate CheckUserDelete and DeleteUser definitions in the core deleter service
rg -n -F -e 'CheckUserDelete(' -e 'DeleteUser(' core/deleter/service.go
# Read the user delete path, including blocker construction
nl -ba core/deleter/service.go | sed -n '600,730p'
# Read orgDeleteBlockers, where fromProvider is consumed
nl -ba core/deleter/service.go | sed -n '852,900p'# Read allBlockers and the UserDeleteCheck type
nl -ba core/deleter/deleter.go | sed -n '55,110p'
Repository: raystack/frontier
Length of output: 4094
Script executed:
# Locate and read invoiceBlockers
rg -n -F -e 'func (d Service) invoiceBlockers' core/deleter/service.go
# Read the body after its definition line; adjust the range once the line number is known
nl -ba core/deleter/service.go | sed -n '900,1000p'# Read the check-to-PB transform in the handler
nl -ba internal/api/v1beta1connect/user.go | sed -n '474,500p'
Repository: raystack/frontier
Length of output: 6096
Script executed:
nl -ba core/deleter/service.go | sed -n '1000,1030p'
Repository: raystack/frontier
Length of output: 1454
Show the refusal's violations in the blocked step, not the confirmation.
When DeleteUser refuses, onDeleteRefused re-runs the check. The check reads local invoice rows, so it can return canDelete: true while the server blocks on a provider-side invoice. The dialog then shows the confirmation and the "Unable to delete account" toast together. The refusal's blockers are discarded.
onDelete calls onDeleted() inside the try block. If the host callback throws, the catch block shows the generic error toast after the "Account deleted" success toast. The account is already deleted at that point. Run the success path after the try/catch, so only the delete request is covered by the error handling:
DeleteUserDialog runs the whole account delete. It opens first and runs
CheckCurrentUserDelete inside, so it works from any trigger, including a
menu item that has already closed. Open it with a trigger slot, or with a
handle from DeleteUserDialog.createHandle().
When the user is the only owner of an organization with other members,
it asks for an ownership transfer. When an organization that would be
deleted with the account is blocked, it lists the blockers for each
organization. Both go through the deleteBlockedDialog slot. Otherwise it
shows the Delete Account confirmation, which needs the user's email and
an acknowledgement. A refused delete checks again to show what blocks it.
ProfileView takes onAccountDeleted. When it is set, the view shows a
Delete account section. The SDK does not navigate after the delete.
ProfileView shows the Delete Account section only when enableUserDelete
is set. It is off by default, so a host app turns it on when it is ready
to handle the delete with onAccountDeleted.
The account delete dialogs follow the design: the ownership dialog has
two paragraphs and no list, and the confirmation has the new field label
and acknowledgement text.
A click on Delete Account now runs CheckCurrentUserDelete first. The
trigger slot gets loading, so the button is disabled and shows a spinner
with its label while the check runs. Then the ownership, blocked or
confirmation dialog opens. A failed check shows a toast.
The handle from DeleteUserDialog.createHandle() is now { open() }. It runs
the same check, so a menu item that has already closed can still start
the delete.
DeleteBlockedDialog now renders its own AlertDialog.Content, so the
blocked step renders it directly and only the confirmation form is
wrapped in AlertDialog.Content.
The blockers prop draws one list with no heading. An account delete can be blocked in more than one workspace, so a host that words its own lines needs a heading for each workspace. The groups prop takes a title and blockers for each group.
Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.
description: `You’re the only owner of one or more ${t.organization({
case: 'lower',
plural: true
})}. Before deleting your account, transfer ownership of each affected ${org} to another member.\n\nOnce ownership has been transferred, you can proceed with account deletion.`,
cd web/sdk/client 2>/dev/null ||true
nl -ba components/delete-user-dialog/delete-user-dialog.tsx | sed -n '1,240p'
Repository: raystack/frontier
Length of output: 8342
List the sole-owned organization titles in the ownership transfer dialog.
When soleOwnerOf is not empty, the default dialog shows only generic "one or more" text and no organization names. The titles are passed in the payload but never rendered, so users cannot tell which organization needs an ownership transfer.
Keep another dialog's starter when this cleanup runs.
The effect cleanup deletes the handle's entry unconditionally. If a second dialog shares this handle, unmounting the first dialog removes the second dialog's starter. handle.open() then does nothing while the second dialog stays mounted. Delete the entry only when it still belongs to this instance.
Disable Delete while the post-refusal eligibility check runs.
When the delete request returns FailedPrecondition, onDeleteRefused starts checkAndOpen. The confirmation form stays mounted and interactive until that check completes. isPending is false by then, so the Delete button re-enables. The user can click Delete again, which sends another deleteCurrentUser request. Overlapping checks can also show duplicate error toasts. Include the recheck state in the Delete button's disabled condition, and show loading while it runs.
The reason will be displayed to describe this comment to others. Learn more.
question: The ticket asks the confirmation step to show what goes away with the account: the orgs deleted, the orgs left, and the number of PATs.
The check already returns these, but they aren't passed to the confirm step. Was this dropped in the design?
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The checkbox isn't linked to the text next to it, so screen readers announce it as an unlabeled checkbox, and clicking the text doesn't tick it.
Could we add an id to the text and aria-labelledby on the checkbox, or wrap both in a <label>?
The reason will be displayed to describe this comment to others. Learn more.
question: We have the names of the orgs where the user is the only owner, but the dialog says "one or more organizations" without listing them.
Is that from the design? If not, listing them would save users some searching.
The reason will be displayed to describe this comment to others. Learn more.
question: This reads blockers from organizationsToDelete and the "only owner" ones from organizationsToLeave.
The response also has a top-level blockers list, which the server leaves empty today.
Should we read that too, so a new server-side blocker shows up without an SDK change?
The reason will be displayed to describe this comment to others. Learn more.
suggestion: DeleteUserDialog needs onDeleted because the SDK doesn't redirect, but here onAccountDeleted is optional.
If a host turns on enableUserDelete and leaves out the callback, the user stays on the profile page after their account is gone.
What do you think about making it required when the flag is on?
The reason will be displayed to describe this comment to others. Learn more.
nit: The demo app's Profile page still renders <ProfileView /> without the flag, so there's no easy place to try this.
Could we turn it on there with a simple redirect? It would help with the test plan items too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
DeleteUserDialogruns the whole account delete. A click runsCheckCurrentUserDeletefirst, then opens the matching dialog. Thetriggerslot gets{ onClick, loading }, so the trigger button is disabled and shows a spinner, with its label unchanged, while the check runs. This matches the org and project delete buttons.handle.open(), using a handle fromDeleteUserDialog.createHandle(). It runs the same check, so a menu item that has already closed can still start the delete.onDeletedis required: the SDK does not navigate.deleteBlockedDialogslot withentity: 'user'.ProfileViewtakesenableUserDelete(off by default) andonAccountDeleted. With the flag on, the view shows a Delete Account section. A host app turns it on when it handles the delete.Stacked on #2041.
Test plan
tsc --noEmitshows no new errors, eslint,pnpm buildonDeletedrunsProfileViewwithoutenableUserDeleteshows no Delete Account section