Skip to content

feat(connections): disconnect, stranding guard, and Set-a-password CTA - #42

Merged
amrtgaber merged 1 commit into
mainfrom
feat/account-disconnect
May 27, 2026
Merged

feat(connections): disconnect, stranding guard, and Set-a-password CTA#42
amrtgaber merged 1 commit into
mainfrom
feat/account-disconnect

Conversation

@amrtgaber

Copy link
Copy Markdown
Contributor

PR-B of two for #39. Builds on #41 (Connect flow) to close out the frontend portion of the issue.

Summary

  • Plumb has_usable_password from /auth/me through the auth context (defaults to false on missing/stale responses).
  • Disconnect button per linked provider on /profile. Disabled with an inline hint ("You'd have no way to sign in. Set a password first or link another provider.") when removing it would strand the user — mirrors the server-side rule client-side so users don't have to click to learn it's blocked:
    canUnlink = otherConnections.length > 0
              || (hasUsablePassword && email)
  • DELETE /auth/me/connections/{provider} on click:
    • 204 → refresh /auth/me/connections + /auth/me, toast "Disconnected ."
    • 409 unlink_would_strand_user → render the message + remediation list inline (amber alert). Remediation strings matching /set a password/ get a "Start now" Link to /forgot-password?email=<email>. Re-syncs connections + auth in case the server saw a different reality.
    • 404 connection_not_found → refresh connections, toast "That connection is already disconnected."
  • "Set a password" CTA in the connections section when has_usable_password=false, linking to /forgot-password?email=<email>.
  • forgot-password route now accepts ?email= via validateSearch and prefills the input. The "authenticated user → /profile" guard relaxes when ?email= is present so the CTA works without forcing a logout.

Closes

Together with #41, the frontend portion of #39 (Steam associate flow + connect/disconnect settings UI) is done.

Test plan

Unit (vitest, 6 new tests, 45 total — all passing)

  • forgot-password-page.test.tsx — empty default, ?email= prefill.
  • profile-page.test.tsx — Disconnect disabled with helper text when stranding, DELETE on click + list refresh + success toast, 409 → rendered remediation list with "Start now" Link, "Set a password" CTA visible when has_usable_password=false.
  • All other auth-context test contexts updated for the new hasUsablePassword field (default false for unauth, true for the existing auth fixtures).

What's not in this PR

  • E2E re-run. PR-A (feat(connections): account linking — Connect flow + callback purpose dispatch #41) already verified the connections section round-trip live against the local API + Postgres; the net-new logic here (Disconnect button rendering, stranding guard, 409 remediation rendering, CTA, forgot-password prefill) is mechanical UI on top of the same wiring and is fully covered by the new unit tests. Happy to run a fresh Playwright session if you'd prefer it before merging — just ask.

Reviewer checklist

  • pnpm test:run
  • Locally: log in as a user with both Google and Steam linked → both rows show Disconnect, both enabled.
  • Log in as a user with has_usable_password=false and only Google linked → Disconnect shows but is disabled with the "no way to sign in" hint, and the "Set a password" CTA appears below.
  • Click the CTA → lands on /forgot-password with the email prefilled.

PR-B of two for #39. Builds on the Connect flow from #41 to close out the
issue: removal of links, the safety rule, and the OAuth-first password
on-ramp.

- Plumb has_usable_password from /auth/me into the auth context. Default
  to false on missing/stale responses so the UI stays conservative.
- Disconnect button per linked provider. Disabled with an inline hint
  ("You'd have no way to sign in. Set a password first or link another
  provider.") when removing it would strand the user. Mirrors the
  server-side rule so users don't have to click to learn it's blocked:
    canUnlink = otherConnections.length > 0
              || (hasUsablePassword && email)
- DELETE /auth/me/connections/{provider} on click. 204 refreshes the
  list + auth state and toasts success. The 409
  unlink_would_strand_user response renders the message + remediation
  list inline; remediation strings matching /set a password/ get a
  "Start now" Link pointing at /forgot-password?email=<theirs>. The 404
  connection_not_found response refreshes the list and shows a generic
  toast.
- "Set a password" CTA on /profile when has_usable_password=false,
  linking to /forgot-password?email=<theirs>. The forgot-password route
  now reads ?email= via validateSearch and prefills the input; the
  authenticated-user guard relaxes when ?email= is present so the CTA
  works without forcing a logout.

Closes the frontend portion of #39 once both PRs are on main.
@netlify

netlify Bot commented May 26, 2026

Copy link
Copy Markdown

Deploy Preview for criticalbit-auth-web ready!

Name Link
🔨 Latest commit 6b2898c
🔍 Latest deploy log https://app.netlify.com/projects/criticalbit-auth-web/deploys/6a16188947ee530007bbfcc1
😎 Deploy Preview https://deploy-preview-42--criticalbit-auth-web.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@amrtgaber
amrtgaber merged commit 58158a1 into main May 27, 2026
6 checks passed
@amrtgaber
amrtgaber deleted the feat/account-disconnect branch May 27, 2026 03:11
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.

1 participant