Skip to content

feat(profile): Delete My Account button + flow#2288

Open
kushagrasarathe wants to merge 12 commits into
devfrom
feat/account-deletion
Open

feat(profile): Delete My Account button + flow#2288
kushagrasarathe wants to merge 12 commits into
devfrom
feat/account-deletion

Conversation

@kushagrasarathe

Copy link
Copy Markdown
Contributor

What & why

Frontend for self-service Delete My Account (TASK-20092). Pairs with peanut-api-ts#1067 (backend endpoint + login guard). V1 disables the account and shows a "deleted within 30 days" notice.

Changes

  • usersApi.requestDeletion() (src/services/users.ts) → POST /users/me/delete.
  • DeleteAccountButton (src/components/Settings/): a red underlined text link (text-error) that opens a two-step ActionModal:
    1. Confirm — animated sad peanut mascot, "Aw, you're leaving?" copy, Yes, delete it / Never mind, I'll stay.
    2. Goodbye — animated crying peanut mascot, "We'll miss you" copy → clears local session and signs the user out (can't log back in; blocked server-side).
  • Lives on the Profile screen under "Log out" (the reachable account surface — /settings is an orphan route).
  • PostHog events: delete_account_{initiated,confirmed,failed}.

Tests

Service test src/services/__tests__/users.test.ts (happy path + error). Local: typecheck clean, jest green (the one failing suite — countryCurrencyMapping flag-asset coverage — is a pre-existing env artifact: public/flags/*.svg are build-generated, unrelated to this PR).

Cross-repo

Requires peanut-api-ts#1067 deployed for the endpoint + login block.

🤖 Generated with Claude Code

Self-service account deletion (V1). A stroke-styled button in Settings
opens a scary confirmation modal; on confirm it POSTs to /users/me/delete
(disables the account + starts the 30-day data-deletion clock), shows the
'deleted within 30 days' notice, then clears the local session and signs
the user out — they can no longer log back in (login is blocked server-side).

Fires delete_account_{initiated,confirmed,failed} PostHog events.
…tton

- move requestDeletion onto usersApi (it's a /users endpoint) and drop the
  one-method accountApi service
- collapse isOpen+phase into a single modalState enum (kills the impossible
  closed+done state), extract the duplicated modal-lock condition, drop the
  unused catch binding
The /settings route is an orphan (nothing in the app links to it), so the
delete button was effectively invisible. Move it under 'Log out' on the
Profile screen — the real account surface users reach via the home avatar —
and revert the dead /settings edit.
Swap the tiny alert icon for the animated Peanut mascots — sad peanut on
the 'are you sure?' confirm step, crying peanut on the goodbye step — and
warm up the copy/CTAs ('Never mind, I'll stay' / 'Goodbye'). Keeps the
honest bits: permanent, disabled now, data wiped within 30 days.
Drop the stroke button chrome for a plain underlined red text link, a
softer/standard treatment for a destructive secondary action.
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jun 26, 2026 4:44pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@kushagrasarathe, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 12 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0474606-10e4-4cec-965b-5fa2cd8e7a80

📥 Commits

Reviewing files that changed from the base of the PR and between 9bcbe5f and eb6f227.

📒 Files selected for processing (2)
  • src/components/Settings/__tests__/DeleteAccountButton.test.tsx
  • src/lib/validation/recipient.ts

Walkthrough

Adds account-deletion support across analytics, API handling, modal UI, profile integration, and username validation.

Changes

Account deletion flow

Layer / File(s) Summary
Deletion events and API
src/constants/analytics.consts.ts, src/services/users.ts, src/services/__tests__/users.test.ts
Adds account-deletion analytics event names and a usersApi.requestDeletion() POST call to /users/me/delete, with tests for success and failure responses.
Delete-account modal flow
src/components/Settings/DeleteAccountButton.tsx
Adds a client-side delete-account button that opens a modal, tracks initiation/confirmation/failure, calls the deletion API, handles the done state, and logs the user out locally.
Profile footer integration
src/components/Profile/index.tsx
Renders DeleteAccountButton in the profile footer and updates the surrounding logout section layout and label.
Deletion-aware username validation
src/lib/validation/recipient.ts
Changes username verification to use GET /users/username/{username} so deletion-requested accounts are treated as unavailable by this validation path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly describes the added delete-account button and flow.
Description check ✅ Passed The description matches the changeset and explains the account-deletion flow, tests, and placement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5764.4 → 5772.67 (+8.27)
Findings: +2 net (+9 new, -7 resolved)

🆕 New findings (9)

  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 34.72, SLOC 149
  • medium high-mdd — src/components/Profile/index.tsx:22 — Profile: MDD 43.1 (uses across many lines from declarations)
  • medium high-mdd — src/components/Settings/DeleteAccountButton.tsx:21 — DeleteAccountButton: MDD 39.4 (uses across many lines from declarations)
  • medium hotspot — src/components/Profile/index.tsx — 25 commits, +69/-129 lines since 6 months ago
  • medium complexity — src/lib/validation/recipient.ts — CC 14, MI 57.39, SLOC 67
  • medium complexity — src/components/Settings/DeleteAccountButton.tsx — CC 13, MI 62.73, SLOC 60
  • medium complexity — src/services/users.ts — CC 6, MI 62.85, SLOC 48
  • low high-mdd — src/lib/validation/recipient.ts:12 — validateAndResolveRecipient: MDD 16.3 (uses across many lines from declarations)
  • low missing-return-type — src/components/Profile/index.tsx:22 — Profile: exported fn missing return type annotation

✅ Resolved (7)

  • src/constants/analytics.consts.ts — CC 1, MI 34.96, SLOC 146
  • src/components/Profile/index.tsx:21 — Profile: MDD 42.9 (uses across many lines from declarations)
  • src/components/Profile/index.tsx — 23 commits, +64/-126 lines since 6 months ago
  • src/lib/validation/recipient.ts — CC 15, MI 57.49, SLOC 66
  • src/services/users.ts — CC 4, MI 62.94, SLOC 41
  • src/lib/validation/recipient.ts:12 — validateAndResolveRecipient: MDD 16.0 (uses across many lines from declarations)
  • src/components/Profile/index.tsx:21 — Profile: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Settings/DeleteAccountButton.tsx 0.0 7.0 +7.0

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1571 ran, 0 failed, 0 skipped, 23.2s

📊 Coverage (unit)

metric %
statements 53.6%
branches 36.5%
functions 41.2%
lines 53.6%
⏱ 10 slowest test cases
time test
0.5s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.1s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in createOnrampForGuest
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid German IBAN
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces 2
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid UK IBAN with spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

ValidatedUsernameWrapper validated via HEAD, which still reports a deleted
user's username as existing (kept that way to reserve the name). Switch the
recipient validator to GET — that route now 404s for deactivated accounts —
so the public profile + /send + /request render the 'we don't know any
@username' view for deleted users. Signup's availability check is a
separate HEAD call, so username reuse stays blocked.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/validation/recipient.ts (1)

85-88: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Collapse the username validation and lookup into one fetch.

After this GET succeeds, Line 47 still performs another user lookup, so the happy path now does two sequential network calls. Since this method only returns a boolean, the payload from the first GET is thrown away. Consider reusing the fetched user here or letting getByUsername drive the validation path directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/validation/recipient.ts` around lines 85 - 88, The username
validation path is making two sequential network calls because
verifyPeanutUsername fetches the user and the happy path still does a separate
lookup afterward. Update verifyPeanutUsername and the related getByUsername flow
in recipient.ts so the initial serverFetch result is reused for validation
instead of discarded, or have getByUsername perform the validation directly,
keeping the method behavior boolean-only without the extra fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/validation/recipient.ts`:
- Line 87: The username lookup in recipient validation is building a URL path
segment directly from raw input, which can be altered by characters like /, ? or
%. Update the serverFetch call in recipient handling to encode the username path
segment with encodeURIComponent before interpolating it. Use the recipient
lookup branch and the username variable as the key place to make this change so
arbitrary input is treated as a single path segment.

---

Nitpick comments:
In `@src/lib/validation/recipient.ts`:
- Around line 85-88: The username validation path is making two sequential
network calls because verifyPeanutUsername fetches the user and the happy path
still does a separate lookup afterward. Update verifyPeanutUsername and the
related getByUsername flow in recipient.ts so the initial serverFetch result is
reused for validation instead of discarded, or have getByUsername perform the
validation directly, keeping the method behavior boolean-only without the extra
fetch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a165d025-baa2-412f-bef0-7659cbc31e06

📥 Commits

Reviewing files that changed from the base of the PR and between 894f32b and 9bcbe5f.

📒 Files selected for processing (1)
  • src/lib/validation/recipient.ts

Comment thread src/lib/validation/recipient.ts Outdated
CodeRabbit: guard the username path segment against /, ?, % in arbitrary input.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Settings/DeleteAccountButton.tsx`:
- Around line 53-59: The DeleteAccountButton flow can trap the user in a
non-dismissible “done” modal if logout fails because finish() calls logoutUser({
skipBackendCall: true }) without guaranteeing navigation and lockModal blocks
dismissal whenever modalState is 'done'. Update the DeleteAccountButton logic so
a failed logout path can recover, either by not locking the modal until logout
succeeds or by clearing/rewinding modalState when logoutUser reports an error;
use finish(), lockModal, and the authContext logoutUser behavior as the key
places to adjust.
- Around line 37-49: Add a synchronous re-entry guard to confirmDelete so it
exits immediately if a deletion is already in progress, instead of relying only
on setIsSubmitting in DeleteAccountButton. Check the current submitting state at
the start of confirmDelete before calling posthog.capture or
usersApi.requestDeletion, and return early on a repeated click to prevent
duplicate DELETE /users/me/delete requests and duplicate
ANALYTICS_EVENTS.DELETE_ACCOUNT_CONFIRMED events. Keep the existing
try/catch/finally flow unchanged otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f270f33b-d5a5-4422-8a34-9834f14d36d1

📥 Commits

Reviewing files that changed from the base of the PR and between 6866c4d and 9bcbe5f.

📒 Files selected for processing (6)
  • src/components/Profile/index.tsx
  • src/components/Settings/DeleteAccountButton.tsx
  • src/constants/analytics.consts.ts
  • src/lib/validation/recipient.ts
  • src/services/__tests__/users.test.ts
  • src/services/users.ts

Comment on lines +37 to +49
const confirmDelete = async () => {
setIsSubmitting(true)
posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_CONFIRMED)
try {
await usersApi.requestDeletion()
setModalState('done')
} catch {
posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_FAILED)
toast.error('Could not delete your account. Please try again.')
} finally {
setIsSubmitting(false)
}
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add a synchronous re-entry guard to confirmDelete().

Line 38 only schedules the disabled state for the next render, so a fast double-click can still fire POST /users/me/delete twice and emit duplicate delete_account_confirmed events. Guard the handler itself before sending the request.

Suggested fix
     const confirmDelete = async () => {
+        if (isSubmitting) return
         setIsSubmitting(true)
         posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_CONFIRMED)
         try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const confirmDelete = async () => {
setIsSubmitting(true)
posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_CONFIRMED)
try {
await usersApi.requestDeletion()
setModalState('done')
} catch {
posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_FAILED)
toast.error('Could not delete your account. Please try again.')
} finally {
setIsSubmitting(false)
}
}
const confirmDelete = async () => {
if (isSubmitting) return
setIsSubmitting(true)
posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_CONFIRMED)
try {
await usersApi.requestDeletion()
setModalState('done')
} catch {
posthog.capture(ANALYTICS_EVENTS.DELETE_ACCOUNT_FAILED)
toast.error('Could not delete your account. Please try again.')
} finally {
setIsSubmitting(false)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Settings/DeleteAccountButton.tsx` around lines 37 - 49, Add a
synchronous re-entry guard to confirmDelete so it exits immediately if a
deletion is already in progress, instead of relying only on setIsSubmitting in
DeleteAccountButton. Check the current submitting state at the start of
confirmDelete before calling posthog.capture or usersApi.requestDeletion, and
return early on a repeated click to prevent duplicate DELETE /users/me/delete
requests and duplicate ANALYTICS_EVENTS.DELETE_ACCOUNT_CONFIRMED events. Keep
the existing try/catch/finally flow unchanged otherwise.

Comment on lines +53 to +59
const finish = () => {
logoutUser({ skipBackendCall: true })
}

// Once submitting (or on the final notice) the modal can't be dismissed —
// the user must complete the flow through the CTA.
const lockModal = isSubmitting || modalState === 'done'

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The locked “done” state has no recovery path if logout fails.

logoutUser({ skipBackendCall: true }) is not guaranteed to redirect; src/context/authContext.tsx:241-274 catches failures and only shows a toast. Because lockModal stays true for every 'done' state, a logout error leaves the user stuck behind a non-dismissible modal with the same failing CTA.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Settings/DeleteAccountButton.tsx` around lines 53 - 59, The
DeleteAccountButton flow can trap the user in a non-dismissible “done” modal if
logout fails because finish() calls logoutUser({ skipBackendCall: true })
without guaranteeing navigation and lockModal blocks dismissal whenever
modalState is 'done'. Update the DeleteAccountButton logic so a failed logout
path can recover, either by not locking the modal until logout succeeds or by
clearing/rewinding modalState when logoutUser reports an error; use finish(),
lockModal, and the authContext logoutUser behavior as the key places to adjust.

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

QA testing guide — Delete My Account (V1)

Setup: a logged-in test account (note its username). Needs peanut-api-ts#1067 deployed.

Happy path

  1. Profile (home → avatar) → tap Delete My Account (red underlined link under Log out).
  2. Confirm modal: sad-peanut, "Aw, you're leaving?" → tap Yes, delete it.
  3. Goodbye modal: crying-peanut, "We'll miss you" → tap Goodbye → you're signed out (land on /setup).
  4. Try to log in again with the same passkey → blocked (cannot get back in).

Cancel path

  • Open the modal → Never mind, I'll stay (or tap the backdrop) → modal closes, account untouched, still logged in.

Invisibility (check from a second account or logged-out browser)

  • peanut.me/<deleted-username>"We don't know any @".
  • peanut.me/send/<deleted-username> → same not-found.
  • peanut.me/request/<deleted-username> → same not-found.
  • Contacts of an account that previously transacted with them → deleted user no longer appears.

Guardrails

  • The username stays reserved — it can't be re-registered at signup.
  • Error path: if the delete call fails, a toast "Could not delete your account…" shows and you stay logged in; retry works.

Reactivation (ops/support)

  • Set app.users.deactivated_at = NULL for the user → they can log in again and reappear in lookups.

Out of scope (V1): actual 30-day data purge (future), and an existing session's token still works until it expires (re-login is what's blocked).

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Changed files =="
gh pr view 2288 --json files --jq '.files[].path'

echo
echo "== Outline: src/components/Settings/DeleteAccountButton.tsx =="
ast-grep outline src/components/Settings/DeleteAccountButton.tsx --view expanded || true

echo
echo "== src/components/Settings/DeleteAccountButton.tsx =="
sed -n '1,220p' src/components/Settings/DeleteAccountButton.tsx

echo
echo "== src/components/Profile/index.tsx =="
sed -n '1,220p' src/components/Profile/index.tsx

echo
echo "== src/services/users.ts =="
sed -n '1,220p' src/services/users.ts

echo
echo "== src/services/__tests__/users.test.ts =="
sed -n '1,220p' src/services/__tests__/users.test.ts

echo
echo "== src/lib/validation/recipient.ts =="
sed -n '1,220p' src/lib/validation/recipient.ts

echo
echo "== src/constants/analytics.consts.ts =="
sed -n '1,220p' src/constants/analytics.consts.ts

echo
echo "== Search logoutUser definition/usages =="
rg -n -C3 '\blogoutUser\b' src

echo
echo "== Search ActionModal definition/usages =="
rg -n -C2 'ActionModal' src/components src/hooks src/lib src

echo
echo "== Search requestDeletion / usersApi patterns =="
rg -n -C3 '\brequestDeletion\b|\bserverFetch\s*\(' src/services src

echo
echo "== Search verifyPeanutUsername usage =="
rg -n -C3 '\bverifyPeanutUsername\s*\(' src

Length of output: 222427



@kushagrasarathe I reviewed this in depth.

Overall assessment

Risk of new bugs: moderate

  • The delete-account UI itself looks reasonably contained and I don’t see an obvious breaking issue in src/components/Settings/DeleteAccountButton.tsx.
  • The broader risk is actually in the recipient validation change, not the delete flow.

Main concern

In src/lib/validation/recipient.ts, verifyPeanutUsername() now does a GET instead of a HEAD, but the username flow still does:

  • Line 43: await verifyPeanutUsername(recipient)
  • Line 47: await usersApi.getByUsername(recipient)

That means username resolution now becomes GET + GET to the same /users/username/:username resource.

Why this matters

  • Performance / latency: this adds an extra full payload request on a hot path.
  • Bug risk: this path is used in recipient validation, so any change here affects send/request flows, not just settings.
  • Code quality / DRY: we’re fetching the same resource twice.

Recommendation

Consider collapsing this into a single fetch:

  • either make verifyPeanutUsername() return the user payload or null
  • or remove the pre-check and let usersApi.getByUsername() be the source of truth, handling 404 as “invalid username”

That would reduce latency and avoid duplicate API traffic.


Delete-account flow review

For src/components/Settings/DeleteAccountButton.tsx:

Good

  • modal is locked while submitting / after success, which avoids inconsistent intermediate states
  • logoutUser({ skipBackendCall: true }) is sensible after account deletion
  • service method in src/services/users.ts is straightforward and matches existing patterns
  • analytics wiring is consistent

Residual risk

  • The flow has service tests, but not UI-state tests for:
    • confirm → loading → done
    • error toast on failure
    • modal non-dismissable behavior during submit/done
    • logout trigger on “Goodbye”

So the delete feature is probably okay, but its behavioral coverage is thinner than the recipient-validation impact area.


Performance impact

  • Delete flow: negligible; it’s a one-off user action.
  • Recipient validation change: potentially noticeable, because it affects repeated username validation/resolution and now does more network work than before.

Bottom line

If I had to rank the risks in this PR:

  1. Highest concern: duplicate GET /users/username/:username calls after the HEADGET switch
  2. Lower concern: missing UI tests for the new delete-account modal state machine
  3. Low concern: delete-account service integration itself

So I’d call this safe-ish for the account deletion feature, but with a meaningful performance/DRY regression in recipient validation that’s worth fixing before merge.

…tests

CodeRabbit in-depth review follow-ups:
- validateAndResolveRecipient no longer does verifyPeanutUsername (GET) then
  getByUsername (GET) on the same resource — one getByUsername now both validates
  (404 = invalid/deleted) and resolves, removing a duplicate round-trip on the
  send/request hot path.
- add DeleteAccountButton state-machine tests: confirm->success->done->logout,
  error toast on failure, and cancel.
@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai. Addressed in 711deaa:

  • Duplicate GET /users/username/:username (the main concern): validateAndResolveRecipient no longer does verifyPeanutUsername (GET) + getByUsername (GET) on the same resource — one getByUsername now both validates (404 = invalid/deleted) and resolves, removing the duplicate round-trip on the send/request path.
  • Thin UI coverage: added DeleteAccountButton state-machine tests — confirm→loading→success→done→logout, error-toast on failure, and cancel.

Note: ValidatedUsernameWrapper still validates via a GET and then PublicProfile fetches the profile — that's a validation-gate-then-render across two components, not a same-call duplicate. Left as-is for now (optimizable later with a shared query cache); the hot same-function duplicate is the one that's fixed.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@kushagrasarathe Thanks for the detailed follow-up — that addresses the two main points I raised:

  • removing the same-function duplicate GET /users/username/:username in validateAndResolveRecipient
  • adding UI-state coverage for the delete-account modal flow

Your note about ValidatedUsernameWrapper + PublicProfile being a cross-component validation/render pattern rather than the hot duplicate I flagged also makes sense.

I’ll review the updated changes now.


✏️ Learnings added
Learnt from: kushagrasarathe
URL: https://github.com/peanutprotocol/peanut-ui/pull/2288

Timestamp: 2026-06-26T16:26:18.650Z
Learning: In the peanut-ui codebase, the duplicate username lookup concern was specifically about `validateAndResolveRecipient` doing both `verifyPeanutUsername()` and `usersApi.getByUsername()` against `/users/username/:username` in the same call path. A remaining pattern where `ValidatedUsernameWrapper` validates via GET and `PublicProfile` fetches profile data is a separate cross-component validation-gate-then-render flow and is intentionally left as-is for now.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kushagrasarathe

Copy link
Copy Markdown
Contributor Author

@coderabbitai re-review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PR metadata =="
gh pr view 2288 --json number,title,baseRefName,headRefName,files,commits --jq '{
  number, title, baseRefName, headRefName,
  files: [.files[].path],
  commits: [.commits[].oid[0:10]]
}'

echo
echo "== Changed files =="
gh pr view 2288 --json files --jq '.files[].path'

echo
echo "== Latest diff summary =="
git diff --stat "$(gh pr view 2288 --json baseRefName --jq .baseRefName)"...HEAD

echo
echo "== Find recipient-validation symbols =="
rg -n -C3 '\bvalidateAndResolveRecipient\b|\bverifyPeanutUsername\b|\bgetByUsername\b|\bValidatedUsernameWrapper\b|\bPublicProfile\b' src

echo
echo "== Outline: src/lib/validation/recipient.ts =="
ast-grep outline src/lib/validation/recipient.ts --view expanded || true

echo
echo "== src/lib/validation/recipient.ts =="
sed -n '1,240p' src/lib/validation/recipient.ts

echo
echo "== Locate validateAndResolveRecipient implementation =="
rg -n -C5 '\bvalidateAndResolveRecipient\b' src

echo
echo "== Outline: src/components/Settings/DeleteAccountButton.tsx =="
ast-grep outline src/components/Settings/DeleteAccountButton.tsx --view expanded || true

echo
echo "== src/components/Settings/DeleteAccountButton.tsx =="
sed -n '1,260p' src/components/Settings/DeleteAccountButton.tsx

echo
echo "== Find DeleteAccountButton tests =="
rg -n -C3 'DeleteAccountButton|requestDeletion|logoutUser|toast' src --iglob '*test*' --iglob '*spec*'

echo
echo "== src/services/__tests__/users.test.ts =="
sed -n '1,240p' src/services/__tests__/users.test.ts

echo
echo "== Any component tests near DeleteAccountButton =="
fd -i 'DeleteAccountButton' src

echo
echo "== src/services/users.ts =="
sed -n '1,220p' src/services/users.ts

echo
echo "== src/components/Profile/index.tsx =="
sed -n '1,220p' src/components/Profile/index.tsx

echo
echo "== logoutUser definition/usages =="
rg -n -C4 '\blogoutUser\b' src

echo
echo "== ActionModal definition/usages =="
rg -n -C3 '\bActionModal\b' src/components src/hooks src/lib src

Length of output: 3457


🏁 Script executed:

#!/bin/bash
set -u

echo "== PR diff (file headers only) =="
gh pr diff 2288 --patch | sed -n '1,260p' || true

echo
echo "== Changed files =="
gh pr view 2288 --json files --jq '.files[].path' || true

echo
echo "== Outline: src/lib/validation/recipient.ts =="
ast-grep outline src/lib/validation/recipient.ts --view expanded || true

echo
echo "== src/lib/validation/recipient.ts =="
sed -n '1,260p' src/lib/validation/recipient.ts || true

echo
echo "== Locate validateAndResolveRecipient implementation/usages =="
rg -n -C5 '\bvalidateAndResolveRecipient\b' src || true

echo
echo "== Search verifyPeanutUsername/getByUsername call sites =="
rg -n -C3 '\bverifyPeanutUsername\s*\(|\bgetByUsername\s*\(' src || true

echo
echo "== Outline: src/components/Settings/DeleteAccountButton.tsx =="
ast-grep outline src/components/Settings/DeleteAccountButton.tsx --view expanded || true

echo
echo "== src/components/Settings/DeleteAccountButton.tsx =="
sed -n '1,280p' src/components/Settings/DeleteAccountButton.tsx || true

echo
echo "== src/components/Settings/__tests__/DeleteAccountButton.test.tsx =="
sed -n '1,320p' src/components/Settings/__tests__/DeleteAccountButton.test.tsx || true

echo
echo "== src/services/users.ts =="
sed -n '1,220p' src/services/users.ts || true

echo
echo "== src/services/__tests__/users.test.ts =="
sed -n '1,240p' src/services/__tests__/users.test.ts || true

echo
echo "== src/components/Profile/index.tsx =="
sed -n '1,240p' src/components/Profile/index.tsx || true

echo
echo "== logoutUser definition/usages =="
rg -n -C4 '\blogoutUser\b' src || true

echo
echo "== ActionModal definition/usages =="
rg -n -C4 '\bActionModal\b' src || true

Length of output: 166448



@kushagrasarathe I re-reviewed the updated changes in depth.

Overall assessment

This looks materially better than the previous revision.

  • Risk of new bugs / breakage: low to low-moderate
  • Performance / latency: improved overall
  • Code quality / DRY: better than before

What improved

1. Hot-path duplicate username fetch is fixed

In src/lib/validation/recipient.ts, validateAndResolveRecipient() now uses a single usersApi.getByUsername() call for USERNAME recipients instead of validate-then-fetch against the same endpoint.

That removes the main risk I called out before:

  • less network work on send/request recipient resolution
  • lower latency on a hot path
  • cleaner single-source-of-truth logic

So on the recipient-validation side, this is now a real improvement, not just a neutral change.

2. Delete flow state handling is cleaner

src/components/Settings/DeleteAccountButton.tsx now uses:

  • modalState: 'closed' | 'confirm' | 'done'
  • a derived lockModal flag

That’s a nice cleanup:

  • removes the impossible closed + done style combination from the earlier shape
  • keeps the modal logic easier to reason about
  • avoids repeating the same lock condition in multiple props

3. Service placement is more coherent

Moving deletion onto usersApi.requestDeletion() is the right DRY/API-boundary choice since this is a /users/... endpoint.

4. UI coverage is much better

src/components/Settings/__tests__/DeleteAccountButton.test.tsx now covers:

  • modal open + initiated event
  • confirm → success → done → logout
  • error toast path
  • cancel path

That meaningfully reduces risk around the new feature.


Residual risk

I only see one notable remaining gap, and it’s relatively small:

Modal lock behavior still isn’t truly tested

The test file mocks ActionModal to a minimal title/buttons shell, so it doesn’t actually verify:

  • backdrop dismissal is blocked during submit/done
  • close button is hidden during submit/done
  • preventClose wiring behaves correctly

So while the component code looks right, the tests currently validate the state machine, not the real modal-dismissal mechanics.

I’d classify that as:

  • non-blocking
  • low severity
  • worth tightening later if this modal pattern becomes more common

Performance / latency

Net result: better than before

  • Delete-account flow itself: negligible runtime impact; it’s a one-off user action.
  • Recipient validation path: improved, because the same-call duplicate GET /users/username/:username is gone.
  • Remaining cross-component GET pattern (ValidatedUsernameWrapper + PublicProfile): still there, but as you noted, that’s a separate validation-then-render pattern, not the hot-path regression I flagged.

So I no longer see a meaningful perf concern in this PR.


Code quality / DRY

Current state looks good:

  • validateAndResolveRecipient() is now drier and more direct
  • DeleteAccountButton state modeling is cleaner
  • lockModal avoids repeated boolean logic
  • requestDeletion() lives in the right service

I don’t see a new DRY regression in the updated patch.


Bottom line

I’d now call this PR:

  • safe for the delete-account feature
  • safer than the prior revision
  • performance-positive overall because of the recipient-path cleanup

The only remaining caution I’d keep in mind is the test gap around actual modal non-dismissability, but I would not treat that as merge-blocking from what I can see here.

✅ Action performed

Full review finished.

CodeRabbit re-review nit: the modal mock now surfaces preventClose +
hideModalCloseButton as data-attrs, and a new test asserts the modal is
unlocked on confirm and locked during submit and on the done step.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant