diff --git a/apps/remix/app/routes/_unauthenticated+/signin.tsx b/apps/remix/app/routes/_unauthenticated+/signin.tsx index 3b13832b4b..6f9b9bda09 100644 --- a/apps/remix/app/routes/_unauthenticated+/signin.tsx +++ b/apps/remix/app/routes/_unauthenticated+/signin.tsx @@ -95,6 +95,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { const [searchParams] = useSearchParams(); const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false); + const [isRedirectFailed, setIsRedirectFailed] = useState(false); const errorParam = searchParams.get('error'); const signupError = errorParam ? SIGNUP_ERROR_MESSAGES[errorParam] : undefined; @@ -128,10 +129,14 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { return; } - void authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }); + authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }).catch(() => { + // Fall back to the manual form (retry button, break-glass door) instead + // of leaving the user on the spinner forever when the IdP is unreachable. + setIsRedirectFailed(true); + }); }, [shouldRedirectToOIDC, returnTo]); - if (shouldRedirectToOIDC) { + if (shouldRedirectToOIDC && !isRedirectFailed) { return (
diff --git a/apps/remix/app/routes/_unauthenticated+/signup.tsx b/apps/remix/app/routes/_unauthenticated+/signup.tsx index 7a254457d2..313fa29de0 100644 --- a/apps/remix/app/routes/_unauthenticated+/signup.tsx +++ b/apps/remix/app/routes/_unauthenticated+/signup.tsx @@ -12,7 +12,7 @@ import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-vali import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { Loader2Icon } from 'lucide-react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { redirect, useSearchParams } from 'react-router'; import { SignUpForm } from '~/components/forms/signup'; @@ -78,6 +78,8 @@ export default function SignUp({ loaderData }: Route.ComponentProps) { } = loaderData; const [searchParams] = useSearchParams(); + const [isRedirectFailed, setIsRedirectFailed] = useState(false); + const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false); // Suppress the automatic redirect when the user asked for the manual form // via ?direct=1, or when a previous OIDC attempt bounced back with an error @@ -85,17 +87,33 @@ export default function SignUp({ loaderData }: Route.ComponentProps) { const isDirectEntry = searchParams.get('direct') === '1'; const hasIdpError = searchParams.get('error') !== null; - const shouldRedirectToOIDC = shouldAutoRedirectToOIDC && !isDirectEntry && !hasIdpError; + useEffect(() => { + const params = new URLSearchParams(window.location.hash.slice(1)); + + setIsEmbeddedRedirect(params.get('embedded') === 'true'); + }, []); + + const shouldRedirectToOIDC = shouldAutoRedirectToOIDC && !isDirectEntry && !hasIdpError && !isEmbeddedRedirect; useEffect(() => { if (!shouldRedirectToOIDC) { return; } - void authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }); + // Guard against the initial render racing the embedded detection above: + // read the hash synchronously so embedded contexts never bounce to the IdP. + if (new URLSearchParams(window.location.hash.slice(1)).get('embedded') === 'true') { + return; + } + + authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }).catch(() => { + // Fall back to the manual form instead of leaving the user on the + // spinner forever when the IdP is unreachable. + setIsRedirectFailed(true); + }); }, [shouldRedirectToOIDC, returnTo]); - if (shouldRedirectToOIDC) { + if (shouldRedirectToOIDC && !isRedirectFailed) { return (