From 8266ed406bcff0ba8d2daf1c9263cf4f5986c3ac Mon Sep 17 00:00:00 2001 From: xan.lol Date: Fri, 01 May 2026 08:29:49 +0000 Subject: [PATCH] fix: return oauth callback error --- src/App.web.tsx | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------- src/screens/Login/AuthCallback.tsx | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- src/screens/Login/index.tsx | 11 +++++++++-- src/state/session/oauth-web-return-url.ts | 15 +++++++++++++++ 4 file(s) changed, 154 insertion(s)(+), 27 deletion(s)(-) diff --git a/src/App.web.tsx b/src/App.web.tsx --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -41,11 +41,20 @@ useSession, useSessionApi, } from '#/state/session' import {getWebOAuthClient} from '#/state/session/oauth-web-client' -import {consumeOAuthReturnUrl} from '#/state/session/oauth-web-return-url' -import {readLastActiveAccount} from '#/state/session/util' +import { + consumeOAuthReturnUrl, + saveOAuthCallbackError, +} from '#/state/session/oauth-web-return-url' +import { + canAttemptSessionResume, + readLastActiveAccount, +} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' -import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' +import { + Provider as LoggedOutViewProvider, + useLoggedOutViewControls, +} from '#/state/shell/logged-out' import {Provider as OnboardingProvider} from '#/state/shell/onboarding' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' @@ -82,6 +91,7 @@ import * as Geo from '#/geolocation' import {Splash} from '#/Splash' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' +import {cleanError} from './lib/strings/errors' // For local development: the OAuth loopback spec requires IP-based origins // (127.0.0.1), not "localhost". The auth server redirects to 127.0.0.1, but @@ -115,6 +125,7 @@ function InnerApp() { const [isReady, setIsReady] = useState(false) const {currentAccount} = useSession() const {resumeSession, login} = useSessionApi() + const {requestSwitchToAccount, setShowLoggedOut} = useLoggedOutViewControls() const theme = useColorModeTheme() const {t: l} = useLingui() const hasCheckedReferrer = useStarterPackEntry() @@ -133,27 +144,71 @@ async function onLaunch(account?: SessionAccount) { try { // Check for OAuth callback params first (loopback redirects to /) if (hasOAuthCallbackParams()) { - const client = getWebOAuthClient() - const result = await client.init() - if (result?.session) { - await login( - { - service: '', - identifier: '', - password: '', - oauthSession: result.session, - }, - 'LoginForm', - ) + try { + const client = getWebOAuthClient() + const result = await client.init() + if (result?.session) { + await login( + { + service: '', + identifier: '', + password: '', + oauthSession: result.session, + }, + 'LoginForm', + ) + + const returnUrl = consumeOAuthReturnUrl() + if (returnUrl) { + window.location.replace(returnUrl) + return + } + + // Clear hash fragment after processing + window.history.replaceState(null, '', window.location.pathname) + return + } + } catch (e) { + const error = + e instanceof Error ? cleanError(e.message) : cleanError(String(e)) + logger.error('OAuth callback failed', { + error: e instanceof Error ? e.message : String(e), + }) const returnUrl = consumeOAuthReturnUrl() - if (returnUrl) { - window.location.replace(returnUrl) - return + if (account && canAttemptSessionResume(account)) { + try { + await resumeSession(account, true) + setShowLoggedOut(false) + if (returnUrl) { + window.history.replaceState(null, '', returnUrl) + } else { + window.history.replaceState( + null, + '', + window.location.pathname, + ) + } + return + } catch (resumeError) { + logger.error('OAuth callback recovery failed', { + error: + resumeError instanceof Error + ? resumeError.message + : String(resumeError), + }) + } } - // Clear hash fragment after processing - window.history.replaceState(null, '', window.location.pathname) + saveOAuthCallbackError(error) + requestSwitchToAccount({ + requestedAccount: account?.did ?? 'none', + }) + if (returnUrl) { + window.history.replaceState(null, '', returnUrl) + } else { + window.history.replaceState(null, '', window.location.pathname) + } return } } @@ -172,7 +227,7 @@ } } const account = readLastActiveAccount() void onLaunch(account) - }, [resumeSession, login]) + }, [resumeSession, login, requestSwitchToAccount, setShowLoggedOut]) useEffect(() => { return listenSessionDropped(() => { diff --git a/src/screens/Login/AuthCallback.tsx b/src/screens/Login/AuthCallback.tsx --- a/src/screens/Login/AuthCallback.tsx +++ b/src/screens/Login/AuthCallback.tsx @@ -3,14 +3,24 @@ import {useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import {replaceWebLocation} from '#/lib/routes/web' +import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' import {useSessionApi} from '#/state/session' import {getWebOAuthClient} from '#/state/session/oauth-web-client' -import {consumeOAuthReturnUrl} from '#/state/session/oauth-web-return-url' +import { + consumeOAuthReturnUrl, + saveOAuthCallbackError, +} from '#/state/session/oauth-web-return-url' +import { + canAttemptSessionResume, + readLastActiveAccount, +} from '#/state/session/util' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' export function AuthCallback() { - const {login} = useSessionApi() + const {login, resumeSession} = useSessionApi() const navigation = useNavigation() + const {requestSwitchToAccount, setShowLoggedOut} = useLoggedOutViewControls() useEffect(() => { void (async () => { @@ -37,13 +47,53 @@ } navigation.replace('Home') } catch (e: unknown) { + const error = + e instanceof Error ? cleanError(e.message) : cleanError(String(e)) logger.error('OAuth callback failed', { error: e instanceof Error ? e.message : String(e), }) - navigation.replace('Home') + + const returnUrl = consumeOAuthReturnUrl() + const lastAccount = readLastActiveAccount() + + if (lastAccount && canAttemptSessionResume(lastAccount)) { + try { + await resumeSession(lastAccount, true) + setShowLoggedOut(false) + if (returnUrl) { + window.history.replaceState(null, '', returnUrl) + } else { + navigation.replace('Home') + } + return + } catch (resumeError) { + logger.error('OAuth callback recovery failed', { + error: + resumeError instanceof Error + ? resumeError.message + : String(resumeError), + }) + } + } + + saveOAuthCallbackError(error) + requestSwitchToAccount({ + requestedAccount: lastAccount?.did ?? 'none', + }) + if (returnUrl) { + window.history.replaceState(null, '', returnUrl) + } else { + navigation.replace('Home') + } } })() - }, [login, navigation]) + }, [ + login, + navigation, + requestSwitchToAccount, + resumeSession, + setShowLoggedOut, + ]) return null } diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -14,6 +14,7 @@ import { getPdsServiceUrlFromIdentityInfo, resolveIdentityUsingAppView, } from '#/state/session/identity-resolver' +import {consumeOAuthCallbackError} from '#/state/session/oauth-web-return-url' import {useLoggedOutView} from '#/state/shell/logged-out' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' @@ -107,10 +108,16 @@ logger.warn(`Failed to fetch service description for ${serviceUrl}`, { error: String(serviceError), }) ax.metric('signin:hostingProviderFailedResolution', {}) - } else { - setError('') } }, [serviceError, serviceUrl, _, ax]) + + useEffect(() => { + const oauthCallbackError = consumeOAuthCallbackError() + if (oauthCallbackError) { + setError(oauthCallbackError) + setCurrentForm(Forms.Login) + } + }, []) const resolveIdentity = useCallback(async (identifier: string) => { setIsResolvingService(true) diff --git a/src/state/session/oauth-web-return-url.ts b/src/state/session/oauth-web-return-url.ts --- a/src/state/session/oauth-web-return-url.ts +++ b/src/state/session/oauth-web-return-url.ts @@ -1,4 +1,5 @@ const OAUTH_RETURN_URL_KEY = 'oauth_return_url' +const OAUTH_CALLBACK_ERROR_KEY = 'oauth_callback_error' export function saveOAuthReturnUrl(url = window.location.href) { if (typeof window === 'undefined') return @@ -23,3 +24,17 @@ } catch { return undefined } } + +export function saveOAuthCallbackError(error: string) { + if (typeof window === 'undefined') return + + window.sessionStorage.setItem(OAUTH_CALLBACK_ERROR_KEY, error) +} + +export function consumeOAuthCallbackError() { + if (typeof window === 'undefined') return undefined + + const error = window.sessionStorage.getItem(OAUTH_CALLBACK_ERROR_KEY) + window.sessionStorage.removeItem(OAUTH_CALLBACK_ERROR_KEY) + return error || undefined +} -- tangled.sh