diff --git a/packages/shared/src/components/auth/AuthOptionsInner.tsx b/packages/shared/src/components/auth/AuthOptionsInner.tsx index 3422fff946..d8b1174efe 100644 --- a/packages/shared/src/components/auth/AuthOptionsInner.tsx +++ b/packages/shared/src/components/auth/AuthOptionsInner.tsx @@ -196,6 +196,23 @@ function AuthOptionsInner({ const [isRegistration, setIsRegistration] = useState(false); const [isSocialAuthLoading, setIsSocialAuthLoading] = useState(false); const windowPopup = useRef(null); + const popupCheckIntervalRef = useRef | null>( + null, + ); + const authFlowCompletedRef = useRef(false); + + const clearPopupCheck = () => { + if (popupCheckIntervalRef.current) { + clearInterval(popupCheckIntervalRef.current); + popupCheckIntervalRef.current = null; + } + }; + + useEffect(() => { + return () => { + clearPopupCheck(); + }; + }, []); const checkForOnboardedUser = async (data: LoggedUser) => { onAuthStateUpdate({ isLoading: true }); @@ -383,6 +400,21 @@ function AuthOptionsInner({ }; const handleLoginMessage = async (e?: MessageEvent) => { + authFlowCompletedRef.current = true; + clearPopupCheck(); + const popup = windowPopup.current; + windowPopup.current = null; + if (popup && !popup.closed) { + popup.close(); + // Retry after a short delay — some browsers defer the close when the + // popup is still settling after a redirect chain. + setTimeout(() => { + if (!popup.closed) { + popup.close(); + } + }, 300); + } + const callbackError = getSocialAuthCallbackError(e?.data); if (callbackError) { setIsSocialAuthLoading(false); @@ -556,6 +588,21 @@ function AuthOptionsInner({ windowPopup.current.location.href = socialUrl; await setChosenProvider(provider); onAuthStateUpdate?.({ isLoading: true }); + + authFlowCompletedRef.current = false; + clearPopupCheck(); + const popup = windowPopup.current; + popupCheckIntervalRef.current = setInterval(() => { + if (!popup || popup.closed) { + clearPopupCheck(); + if (!authFlowCompletedRef.current) { + setIsSocialAuthLoading(false); + onAuthStateUpdate?.({ isLoading: false }); + displayToast(SOCIAL_AUTH_RETRY_MESSAGE); + } + windowPopup.current = null; + } + }, 500); }; const onProviderMessage = async (e: MessageEvent) => { diff --git a/packages/shared/src/lib/betterAuth.ts b/packages/shared/src/lib/betterAuth.ts index dd7c2636db..8bce53a387 100644 --- a/packages/shared/src/lib/betterAuth.ts +++ b/packages/shared/src/lib/betterAuth.ts @@ -164,6 +164,7 @@ const getBetterAuthSocialRedirect = async ( { provider, callbackURL: absoluteCallbackURL, + errorCallbackURL: absoluteCallbackURL, disableRedirect: true, ...(additionalData && { additionalData }), }, diff --git a/packages/webapp/pages/callback.tsx b/packages/webapp/pages/callback.tsx index b360fedf02..4010207ea2 100644 --- a/packages/webapp/pages/callback.tsx +++ b/packages/webapp/pages/callback.tsx @@ -75,7 +75,10 @@ function CallbackPage(): ReactElement | null { // while still allowing a script-opened tab to close itself. Try the // close first and only fall back to a redirect if we know there is no // opener to return to. - if (!window.opener) { + // Skip the redirect when there are error params — the opener handles + // closing the popup and showing the error toast. + const hasError = urlSearchParams.has('error'); + if (!window.opener && !hasError) { setTimeout(() => { window.location.replace('/onboarding'); }, 300);