Skip to content

fix(auth): scope clearAllAppData to active user; fix re-onboarding race; drop dead API call#1816

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/account-switch-scoped-clear
May 15, 2026
Merged

fix(auth): scope clearAllAppData to active user; fix re-onboarding race; drop dead API call#1816
senamakel merged 5 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/account-switch-scoped-clear

Conversation

@YellowSnnowmann
Copy link
Copy Markdown
Contributor

@YellowSnnowmann YellowSnnowmann commented May 15, 2026

Summary

  • Replace window.localStorage.clear() in clearAllAppData with a targeted per-user key sweep — only ${userId}:* keys and OPENHUMAN_ACTIVE_USER_ID are removed; falls back to full clear only for pre-login recovery (no userId)
  • Move sessionStorage.clear() into a finally block so it runs even when a localStorage error is thrown
  • Remove dead userApi.onboardingComplete() call from OnboardingLayout and the onboardingComplete method from userApi.tsPOST /settings/onboarding-complete does not exist on the backend
  • Gate DefaultRedirect's onboarding-vs-home decision on currentUser !== null — prevents routing to /onboarding during the post-login window where the session token has arrived but refresh() hasn't resolved yet

Problem

Three separate bugs all contributed to #983:

  1. Nuclear wipeclearAllAppData called localStorage.clear(), destroying every account's persisted Redux state. Logging out on account B silently wiped account A's chat history, connections, and onboarding flag.
  2. Post-login re-onboarding racetoSignedOutSnapshot() resets onboardingCompleted: false and currentUser: null on logout. The core-state:session-token-updated event sets sessionToken before refresh() resolves, leaving a window where DefaultRedirect saw sessionToken truthy + onboardingCompleted: false and routed returning users to /onboarding instead of /home.
  3. Dead API callPOST /settings/onboarding-complete is not registered in the backend router. Every onboarding completion fired a silent 404.

Solution

  • clearUserScopedStorage(userId) iterates localStorage keys and removes only those prefixed ${userId}:. Other accounts' keys survive. sessionStorage.clear() is in a finally block so it always runs.
  • DefaultRedirect now shows <RouteLoadingScreen /> when sessionToken is set but currentUser is null — waits for the snapshot to settle before making any routing decision.
  • Removed onboardingComplete from userApi.ts and its call-site in OnboardingLayout.tsx.
  • Tests added/updated: clearAllAppData.test.ts asserts cross-account key preservation; new DefaultRedirect.test.tsx (5 tests) covers bootstrapping, no-session, post-login race, new-user onboarding, and returning-user home redirect.

Submission Checklist

  • Tests added or updated
  • Diff coverage ≥ 80% — all changed files covered
  • Coverage matrix updated — N/A: behaviour-only changes
  • No new external network dependencies
  • Linked issue closed via Closes #983

Impact

Related


AI Authored PR Metadata

Commit & Branch

  • Branch: fix/account-switch-scoped-clear

Validation Run

  • pnpm --filter openhuman-app format:check — passed
  • Focused tests: pnpm debug unit src/utils/__tests__/clearAllAppData.test.ts src/pages/onboarding src/components/__tests__/DefaultRedirect.test.tsx — all pass
  • Rust fmt/check — not changed
  • Tauri fmt/check — not changed

Validation Blocked

  • Pre-push ESLint hook: pre-existing react-hooks/set-state-in-effect warnings in unrelated files (BootCheckGate, RotatingTetrahedronCanvas, CommandProvider, etc.) — present on main before this branch; pushed with --no-verify per CLAUDE.md

Behavior Changes

  • clearAllAppData({ userId: 'x' }) now removes only x-prefixed keys; other accounts' data is preserved
  • DefaultRedirect shows a loading screen during post-login snapshot settling instead of routing prematurely to /onboarding
  • Onboarding completion no longer fires a 404 to /settings/onboarding-complete

Summary by CodeRabbit

  • Bug Fixes

    • Fixed race condition in post-login routing that could cause incorrect navigation.
    • Improved multi-user data handling during logout to preserve other users' stored information.
  • Tests

    • Added comprehensive test coverage for post-login routing behavior.
    • Enhanced user-scoped data cleanup verification in existing tests.

Review Change Stack

…boarding-complete call

clearAllAppData previously called window.localStorage.clear(), wiping
every account's persisted Redux state. Now only keys prefixed with the
active userId (e.g. `${userId}:persist:*`) are removed, leaving other
accounts' data intact. Falls back to a full clear only when no userId
is available (pre-login recovery path).

Also removes the userApi.onboardingComplete() call and the corresponding
POST /settings/onboarding-complete from OnboardingLayout — the endpoint
does not exist on the backend and was silently swallowed on every
onboarding completion.

Closes tinyhumansai#983

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@YellowSnnowmann YellowSnnowmann requested a review from a team May 15, 2026 13:07
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af37684a-6437-462d-b62f-b372b41d3be0

📥 Commits

Reviewing files that changed from the base of the PR and between e311aed and 58ea133.

📒 Files selected for processing (2)
  • app/src/components/DefaultRedirect.tsx
  • app/src/components/__tests__/DefaultRedirect.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/components/DefaultRedirect.tsx
  • app/src/components/tests/DefaultRedirect.test.tsx

📝 Walkthrough

Walkthrough

Removes the backend onboardingComplete API call, scopes localStorage clearing to the active user (null fallback clears all), adds a DefaultRedirect post-login guard to avoid race redirects, and updates tests to validate user-scoped clearing and routing behavior.

Changes

Account Data Isolation and Onboarding Cleanup

Layer / File(s) Summary
Remove backend onboarding-complete API
app/src/pages/onboarding/OnboardingLayout.tsx, app/src/services/api/userApi.ts
Removed the userApi import and deleted the userApi.onboardingComplete() call from onboarding's completeAndExit; removed the onboardingComplete endpoint from userApi, leaving getMe.
Implement user-scoped storage clearing
app/src/utils/clearAllAppData.ts
Added ACTIVE_USER_KEY and clearUserScopedStorage(userId) to remove only active-user-prefixed localStorage keys (or clear all when userId is null), always clears sessionStorage, and integrated it into clearAllAppData.
Test user-scoped storage behavior
app/src/utils/__tests__/clearAllAppData.test.ts
Add per-test storage cleanup, seed multi-user persisted keys, verify clearAllAppData({ userId }) deletes only that user's keys and preserves others, and verify fallback localStorage.clear() when userId is omitted.
DefaultRedirect guard and tests
app/src/components/DefaultRedirect.tsx, app/src/components/__tests__/DefaultRedirect.test.tsx
Render RouteLoadingScreen when snapshot.currentUser is null post-login to prevent incorrect routing; add Vitest + RTL tests covering bootstrapping, no session, post-login race, onboarding redirect, and home redirect.

Sequence Diagram

sequenceDiagram
  participant OnboardingLayout
  participant LocalStorage
  participant Analytics
  participant Joyride
  participant Router
  OnboardingLayout->>LocalStorage: persist `onboarding_completed` flag
  OnboardingLayout->>Analytics: fire `onboarding_complete` event
  OnboardingLayout->>Joyride: set walkthrough pending flag (best-effort)
  OnboardingLayout->>Router: navigate to /home
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hopping through the code with care,

I tuck each user's keys right there,
No mixed-up hops from me to you,
Onboarding marks local-first and true,
A tiny hop, and systems woo.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: scoping clearAllAppData to the active user, fixing a re-onboarding race condition in DefaultRedirect, and removing the dead onboardingComplete API call.
Linked Issues check ✅ Passed The PR addresses all key requirements from #983: scopes storage clearing to active user via clearUserScopedStorage(), fixes post-login race via DefaultRedirect loading guard, prevents re-onboarding by preserving per-account state, and adds comprehensive tests for account-switch scenarios.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #983 objectives: storage scoping logic, DefaultRedirect race fix, dead API removal, and related test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@app/src/utils/clearAllAppData.ts`:
- Around line 25-46: The current try block in clearAllAppData contains both
localStorage operations and sessionStorage.clear(), so if any localStorage call
throws the catch executes and sessionStorage.clear() is skipped; fix this in
clearAllAppData by moving sessionStorage.clear() into a finally block (or into
its own try/catch after the existing catch) so it always runs regardless of
localStorage errors, while keeping the existing localStorage logic (prefix loop,
toRemove array, removal of ACTIVE_USER_KEY) and preserving the existing catch
logging for localStorage failures.
🪄 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: 73bb3dd7-bfe7-4294-bac7-fc9262c957aa

📥 Commits

Reviewing files that changed from the base of the PR and between e7c2eb7 and 6fb5a9c.

📒 Files selected for processing (4)
  • app/src/pages/onboarding/OnboardingLayout.tsx
  • app/src/services/api/userApi.ts
  • app/src/utils/__tests__/clearAllAppData.test.ts
  • app/src/utils/clearAllAppData.ts
💤 Files with no reviewable changes (2)
  • app/src/services/api/userApi.ts
  • app/src/pages/onboarding/OnboardingLayout.tsx

Comment thread app/src/utils/clearAllAppData.ts
sessionStorage.clear() was inside the try block so a localStorage throw
would skip it via the catch. Moved it into a finally block so it always
runs regardless of localStorage failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 15, 2026
…n snapshot

After logout, toSignedOutSnapshot() sets onboardingCompleted: false and
currentUser: null. On re-login, the session token can arrive via
core-state:session-token-updated before refresh() resolves — leaving a
window where sessionToken is truthy but the snapshot is still in the
signed-out cleared state.

DefaultRedirect was routing to /onboarding during this window, forcing
re-onboarding for users who had already completed it.

Fix: if sessionToken is set but currentUser is null the snapshot hasn't
settled yet — show the loading screen and wait for the next refresh cycle
before making the routing decision.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 15, 2026
@YellowSnnowmann YellowSnnowmann changed the title fix(auth): scope clearAllAppData to active user; drop non-existent onboarding-complete call fix(auth): scope clearAllAppData to active user; fix re-onboarding race; drop dead API call May 15, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Walkthrough

Solid three-bug fix for #983 — scoped localStorage clear, post-login race guard, and dead API removal. The approach is sound and the scoped deletion logic is well-implemented (collecting keys before removal to avoid mutation-during-iteration is a nice touch). Tests cover the core scenarios well. Two things need attention before this ships: the new loading guard needs a safety net for failed refreshes, and the OnboardingLayout test still mocks the removed API call.

Change Summary

File Change type Description
app/src/utils/clearAllAppData.ts Modified Added clearUserScopedStorage() — scoped localStorage deletion by userId prefix, replacing the nuclear localStorage.clear()
app/src/components/DefaultRedirect.tsx Modified Added !currentUser null guard to prevent premature onboarding redirect during post-login race
app/src/components/__tests__/DefaultRedirect.test.tsx New 5 test cases covering bootstrap, no-session, post-login race, new-user, and returning-user states
app/src/pages/onboarding/OnboardingLayout.tsx Modified Removed dead userApi.onboardingComplete() call (endpoint never existed)
app/src/services/api/userApi.ts Modified Removed onboardingComplete method and apiClient import
app/src/utils/__tests__/clearAllAppData.test.ts Modified Updated tests to assert scoped key deletion and cross-account preservation

// would be wrong for any returning user whose flag is already true.
if (!snapshot.currentUser) {
return <RouteLoadingScreen />;
}
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.

[major] This guard correctly prevents the race condition, but has no timeout or fallback. If refresh() fails after login (network error, core crash, etc.), currentUser stays null forever and the user is stuck on <RouteLoadingScreen /> with no way out.

Looking at CoreStateProvider, refresh() failures are caught and logged but don't clear the session token — so this guard would hold indefinitely.

Consider adding a timeout that falls back to clearing the session:

const [waitingForUser, setWaitingForUser] = useState(false);

useEffect(() => {
  if (snapshot.sessionToken && !snapshot.currentUser) {
    setWaitingForUser(true);
    const timer = setTimeout(() => {
      // refresh() likely failed — fall back to signed-out state
      clearSession();
    }, 10_000);
    return () => clearTimeout(timer);
  }
  setWaitingForUser(false);
}, [snapshot.sessionToken, snapshot.currentUser]);

Or at minimum, add debug logging so the stuck state is diagnosable:

if (!snapshot.currentUser) {
  debug('default-redirect')('waiting for currentUser — token set but snapshot not yet refreshed');
  return <RouteLoadingScreen />;
}

expect(screen.queryByText('Home')).not.toBeInTheDocument();
expect(screen.queryByText('Welcome')).not.toBeInTheDocument();
});

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.

[minor] This test only asserts negatives (nothing is visible), but doesn't positively assert the loading screen is shown. If DefaultRedirect accidentally rendered nothing at all, this test would still pass.

Consider adding a positive assertion — e.g., checking for a loading indicator or a data-testid on RouteLoadingScreen.

senamakel added 2 commits May 15, 2026 13:57
Addresses graycyrus PR review on tinyhumansai#1816:
- DefaultRedirect: add console.debug when waiting on currentUser so a stuck
  state (refresh() never resolves after session-token-updated) is diagnosable
  from logs rather than appearing as a silent loading screen.
- DefaultRedirect.test.tsx: positively assert the loading screen renders in
  the post-login race case (previously only asserted negatives, which would
  pass even if the component rendered nothing).
@senamakel senamakel merged commit b4c19b1 into tinyhumansai:main May 15, 2026
23 checks passed
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
…ce; drop dead API call (tinyhumansai#1816)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix account switching causing data loss and forced re-onboarding after re-login

3 participants