fix: show OAuth callback errors instead of an endless spinner - #830
Open
doowta wants to merge 2 commits into
Open
fix: show OAuth callback errors instead of an endless spinner#830doowta wants to merge 2 commits into
doowta wants to merge 2 commits into
Conversation
The callback route checked the `redirect-to-native` flag before it checked `error`, and only cleared that flag on success. So on the desktop and mobile flow a failed callback set the error state, re-rendered, matched the native branch first, and left the user on "Completing authentication..." with no way forward. The error card below it was unreachable. Clear the flag in handleAuthError and move the error branch above the native one. Either change fixes the hang; together they also stop a stale flag from making a later web sign-in deep-link into the desktop app. Also give 409 UserExistsNotLinked its own copy. That case cannot be solved by retrying the same provider, so pointing at "Try Again" sends people back into the same wall. It now says the email already has an account and links to the login screen.
The auth screens showed a generic spinner. This replaces it with the Maple mark walking its own wordmark, M, A, P, L, E. Every letter of the mark on trymaple.ai is a single closed contour with no counter, so all five resample to the same point count and interpolate directly. That is what avoids a morph library: `d` is a lerp between two equal-length rings. Geometry is derived once from the wordmark's own path data, so the animation follows the logo asset instead of drifting from it. No new dependency, 2.1 KB gzipped, roughly 0.03 ms per frame. It paints with currentColor and holds a static mark under prefers-reduced-motion. This is an aesthetic change rather than a fix, so it is a separate commit and can be dropped without touching the one before it.
doowta
marked this pull request as ready for review
August 23, 2026 21:50
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Hi. I was signing in to try Maple and got stuck, so I went looking. It turned out to be a small
thing in the OAuth callback route, not anything to do with auth itself. Sharing in case it's
useful. No pressure either way.
What happens
I have an old account made with email and password. On the desktop app I clicked "Sign in with
Google", the browser opened, Google was happy, and then the page sat on "Processing Google Login /
Completing authentication..." forever. No error, nothing to click.
The backend is already doing the right thing. It answers 409
UserExistsNotLinkedbecause theemail belongs to an account this Google identity isn't linked to. That's correct. Linking on a
matching email would be an account takeover vector, and here it would hand over the seed. The 409
just never reaches the screen.
Why
In
frontend/src/routes/auth.$provider.callback.tsxthe render branches are ordered:redirect-to-nativeis only cleared inhandleSuccessfulAuth, not inhandleAuthError. So on thedesktop path the error state gets set, the component re-renders, hits the native branch first, and
the error card below it is never reached. This isn't specific to the 409. Any callback failure
lands there.
There's a knock-on effect that made it confusing to diagnose from outside.
/desktop-authsetsthat flag in the browser's localStorage on trymaple.ai, and nothing clears it on failure. So
afterwards, a plain web sign-in in the same browser either shows the same stuck spinner, or on
success deep-links to
cloud.opensecret.maple://instead of continuing in the browser. One faileddesktop attempt makes web sign-in look broken too.
#294 covered the deep link failing after a success. This is the error path, which I think just
never had reachable UI.
The change
Small. Clear the flag in
handleAuthError, and move theif (error)branch above the nativebranch. Either one fixes the hang. Both together also stop the stale flag leaking into web sign-in.
I also gave the 409 its own copy, because "try again" is the one thing that can't work here.
Retrying the same provider hits the same wall. It now says what happened and points at the method
that will work.
On not leaking who has an account
This was the first thing I checked, since the message tells someone an account exists. I don't
think it widens disclosure:
own userinfo endpoint, with
email_verifiedenforced infetch_google_user, and afterconsume_statehas validated the one-time state. You have to control the Google account to seeit, so it can't be used to probe other people's addresses.
api.tsmaps "User exists" to "Anaccount with this email already exists"). Web users already saw that sentence. Desktop users saw
nothing at all. Same disclosure, same audience, just no longer a dead end.
It suggests email and password as the likely one.
Happy to soften the wording if you'd rather it said less.
The loading indicator (second commit, take it or leave it)
While I was in there I noticed the auth screens use a generic spinner, so I tried something: the
Maple mark walking its own wordmark, M, A, P, L, E.
Each letter of the mark is a single closed contour, so all five resample to the same point count
and interpolate directly. That means no morph library and no new dependency. It's 2.1 KB gzipped,
about 0.03 ms per frame, and holds a static mark under
prefers-reduced-motion. It paints withcurrentColor, so it takes the surrounding text colour rather than introducing one of its own.The three panels above are the same component, not three variants.
Two things to flag:
figma/home/header-logo.svg), which is a differentlogo from the one the app ships in
public/maple-logo.svg. So it assumes the site's mark is thecurrent one. Tell me if that's wrong.
still stands.
Testing
Added
auth.$provider.callback.test.tsx(5 tests). They fail onmasterand pass with the fix.The first one asserts the desktop path shows the failure instead of "Completing authentication...".
MapleLoadingMark.test.tsxadds 6 more. One pins the frame ordering, another checks the componentstops its animation frame when it unmounts.
Ran what CI runs (
scripts/ci/frontend.sh):format:check,lint,typecheck,test. 775 pass,0 fail. Lint has 0 errors. The one warning in the file I touched is pre-existing, confirmed on an
unmodified checkout.
I also drove the whole flow end to end against a small local stand-in for OpenSecret, so the before
and after above are real screenshots rather than a description. Happy to share that harness, though
you can probably reproduce it faster in your own dev stack.
There's a bigger question underneath this that I didn't touch here. Once you hit that 409 there's
no way to link the two, because there's no endpoint or UI to attach a provider to an existing
account. I wrote that up separately rather than mixing it in. It also looks entangled with the
recovery credential design in opensecret#286, so it felt like a question rather than a patch.