From b03ad5e13045d64dcbeda4ee698277d46a89bc14 Mon Sep 17 00:00:00 2001 From: Maduranga Siriwardena Date: Tue, 23 Jun 2026 21:42:35 +0530 Subject: [PATCH] Redirect when a sign-in flow completes on its first step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 SignIn handled terminal flow statuses (Complete/Error) only after a submission, not on the initial flow load. When a flow completes on its first step — e.g. a reused browser SSO session lets the backend return COMPLETE immediately with no UI components — the gate fell through with no components and spun on a loader forever. Extract the terminal-response handling into a shared helper and run it on the initial load (initializeFlow) as well as after a submission (handleSubmit), so the flow redirects to the callback instead of hanging. --- .../presentation/auth/SignIn/SignIn.tsx | 109 +++++++++++------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/packages/react/src/components/presentation/auth/SignIn/SignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/SignIn.tsx index be9ed29..d07a56f 100644 --- a/packages/react/src/components/presentation/auth/SignIn/SignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/SignIn.tsx @@ -434,6 +434,62 @@ const SignIn: FC = ({ return false; }; + /** + * Handle terminal flow responses (Error and Complete) shared by initializeFlow and handleSubmit. + * Throws on an Error status so the caller's catch block can propagate it to BaseSignIn. + * Returns true when a Complete status was handled (caller should return), false otherwise. + */ + const handleTerminalResponse = async (response: EmbeddedSignInFlowResponse): Promise => { + // Handle Error flow status - flow has failed and is invalidated + if (response.flowStatus === EmbeddedSignInFlowStatus.Error) { + await clearFlowState(); + const err: any = new Error(extractErrorMessage(response, t)); + setError(err); + cleanupFlowUrlParams(); + // Throw the error so it's caught by the catch block and propagated to BaseSignIn + throw err; + } + + if (response.flowStatus === EmbeddedSignInFlowStatus.Complete) { + // Get redirectUrl from response (from /oauth2/auth/callback) or fall back to afterSignInUrl + const redirectUrl: any = (response as any)?.redirectUrl || (response as any)?.redirect_uri; + const finalRedirectUrl: any = redirectUrl || afterSignInUrl; + + // Clear submitting state before redirect + setIsSubmitting(false); + + // Clear all OAuth-related storage on successful completion + setExecutionId(null); + await setChallengeToken(null); + setIsFlowInitialized(false); + sessionStorage.removeItem('thunderid_execution_id'); + try { + const storageManager: any = await getStorageManager(); + await storageManager?.removeHybridDataParameter?.('authId'); + } catch { + logger.warn('Failed to clear authId from hybrid storage after completion.'); + } + + // Clean up OAuth URL params before redirect + cleanupOAuthUrlParams(true); + + if (onSuccess) { + onSuccess({ + redirectUrl: finalRedirectUrl, + ...(response.data || {}), + }); + } + + if (finalRedirectUrl && window?.location) { + window.location.href = finalRedirectUrl; + } + + return true; + } + + return false; + }; + /** * Initialize the authentication flow. * Priority: executionId > applicationId (from context) > applicationId (from URL) @@ -528,6 +584,13 @@ const SignIn: FC = ({ return; } + // Handle a flow that completes (or errors) on the very first step — e.g. a reused SSO + // session lets the backend return COMPLETE immediately with no UI components. Without + // this the UI would fall through with no components and spin forever. + if (await handleTerminalResponse(response)) { + return; + } + const { executionId: normalizedExecutionId, components: normalizedComponents, @@ -764,50 +827,8 @@ const SignIn: FC = ({ meta, ); - // Handle Error flow status - flow has failed and is invalidated - if (response.flowStatus === EmbeddedSignInFlowStatus.Error) { - await clearFlowState(); - const err: any = new Error(extractErrorMessage(response, t)); - setError(err); - cleanupFlowUrlParams(); - // Throw the error so it's caught by the catch block and propagated to BaseSignIn - throw err; - } - - if (response.flowStatus === EmbeddedSignInFlowStatus.Complete) { - // Get redirectUrl from response (from /oauth2/auth/callback) or fall back to afterSignInUrl - const redirectUrl: any = (response as any)?.redirectUrl || (response as any)?.redirect_uri; - const finalRedirectUrl: any = redirectUrl || afterSignInUrl; - - // Clear submitting state before redirect - setIsSubmitting(false); - - // Clear all OAuth-related storage on successful completion - setExecutionId(null); - await setChallengeToken(null); - setIsFlowInitialized(false); - sessionStorage.removeItem('thunderid_execution_id'); - try { - const storageManager: any = await getStorageManager(); - await storageManager?.removeHybridDataParameter?.('authId'); - } catch { - logger.warn('Failed to clear authId from hybrid storage after completion.'); - } - - // Clean up OAuth URL params before redirect - cleanupOAuthUrlParams(true); - - if (onSuccess) { - onSuccess({ - redirectUrl: finalRedirectUrl, - ...(response.data || {}), - }); - } - - if (finalRedirectUrl && window?.location) { - window.location.href = finalRedirectUrl; - } - + // Handle terminal flow statuses (Error throws, Complete redirects and returns true). + if (await handleTerminalResponse(response)) { return; }