Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 65 additions & 44 deletions packages/react/src/components/presentation/auth/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,62 @@ const SignIn: FC<SignInProps> = ({
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<boolean> => {
// 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;
}
Comment on lines +458 to +485

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant slices of SignIn.tsx and the loading condition in BaseSignIn.
sed -n '430,500p' packages/react/src/components/presentation/auth/SignIn/SignIn.tsx
printf '\n---\n'
sed -n '970,1020p' packages/react/src/components/presentation/auth/SignIn/SignIn.tsx
printf '\n---\n'
rg -n "afterSignInUrl|redirectUrl|redirect_uri|isFlowInitialized|isLoading =" packages/react/src/components/presentation/auth/SignIn/SignIn.tsx

Repository: thunder-id/javascript-sdks

Length of output: 4720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '640,680p' packages/react/src/components/presentation/auth/SignIn/SignIn.tsx
printf '\n---\n'
sed -n '680,720p' packages/react/src/components/presentation/auth/SignIn/SignIn.tsx

Repository: thunder-id/javascript-sdks

Length of output: 3131


Handle the no-redirect Complete path explicitly.
setIsFlowInitialized(false) leaves BaseSignIn stuck loading when finalRedirectUrl is empty (isLoading || !isInitialized || !isFlowInitialized stays true). Surface a terminal error/state before returning if there’s no redirect target.

🧰 Tools
🪛 ESLint

[error] 467-467: Unsafe assignment of an any value.

(@typescript-eslint/no-unsafe-assignment)


[error] 467-467: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 468-468: Unsafe call of an any typed value.

(@typescript-eslint/no-unsafe-call)


[error] 468-468: Unsafe member access .removeHybridDataParameter on an any value.

(@typescript-eslint/no-unsafe-member-access)


[error] 478-478: Unsafe assignment of an any value.

(@typescript-eslint/no-unsafe-assignment)


[error] 484-484: Unsafe assignment of an any value.

(@typescript-eslint/no-unsafe-assignment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/components/presentation/auth/SignIn/SignIn.tsx` around
lines 458 - 485, The no-redirect completion path in SignIn leaves BaseSignIn
stuck in a loading state because setIsFlowInitialized(false) is called without
ever surfacing a terminal outcome when finalRedirectUrl is empty. Update the
completion logic in SignIn so that, after clearing OAuth state, it explicitly
handles the empty finalRedirectUrl case by setting a terminal error or completed
state and returning early instead of falling through to the redirect path. Use
the SignIn component flow around setIsFlowInitialized, cleanupOAuthUrlParams,
and onSuccess to ensure BaseSignIn no longer stays blocked on isLoading ||
!isInitialized || !isFlowInitialized.


return true;
}

return false;
};

/**
* Initialize the authentication flow.
* Priority: executionId > applicationId (from context) > applicationId (from URL)
Expand Down Expand Up @@ -528,6 +584,13 @@ const SignIn: FC<SignInProps> = ({
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,
Expand Down Expand Up @@ -764,50 +827,8 @@ const SignIn: FC<SignInProps> = ({
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;
}

Expand Down
Loading