Skip to content

Commit 4d10de8

Browse files
committed
feat: add loading state to BaseSignIn component and integrate with SignIn props
1 parent 6cc7a29 commit 4d10de8

File tree

2 files changed

+76
-63
lines changed

2 files changed

+76
-63
lines changed

packages/react/src/components/presentation/SignIn/BaseSignIn.tsx

Lines changed: 68 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,11 @@ export interface BaseSignInProps {
252252
*/
253253
onFlowChange?: (response: EmbeddedSignInFlowInitiateResponse | EmbeddedSignInFlowHandleResponse) => void;
254254

255+
/**
256+
* Flag to determine the component is ready to be rendered.
257+
*/
258+
isLoading?: boolean;
259+
255260
/**
256261
* Function to initialize authentication flow.
257262
* @returns Promise resolving to the initial authentication response.
@@ -328,6 +333,7 @@ const BaseSignIn: FC<BaseSignInProps> = props => (
328333
const BaseSignInContent: FC<BaseSignInProps> = ({
329334
afterSignInUrl,
330335
onInitialize,
336+
isLoading: externalIsLoading,
331337
onSubmit,
332338
onSuccess,
333339
onError,
@@ -343,15 +349,16 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
343349
const {t} = useTranslation();
344350
const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages} = useFlow();
345351

346-
const [isLoading, setIsLoading] = useState(false);
352+
const [isSignInInitializationRequestLoading, setIsSignInInitializationRequestLoading] = useState(false);
347353
const [isInitialized, setIsInitialized] = useState(false);
348354
const [currentFlow, setCurrentFlow] = useState<EmbeddedSignInFlowInitiateResponse | null>(null);
349355
const [currentAuthenticator, setCurrentAuthenticator] = useState<EmbeddedSignInFlowAuthenticator | null>(null);
350356
const [error, setError] = useState<string | null>(null);
351357
const [messages, setMessages] = useState<Array<{message: string; type: string}>>([]);
352358

353-
// Ref to track if initialization has been attempted to prevent multiple calls
354-
const initializationAttemptedRef = useRef(false);
359+
const isLoading = externalIsLoading || isSignInInitializationRequestLoading;
360+
361+
const reRenderCheckRef = useRef(false);
355362

356363
const formFields: FormField[] =
357364
currentAuthenticator?.metadata?.params?.map(param => ({
@@ -598,7 +605,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
598605
return;
599606
}
600607

601-
setIsLoading(true);
608+
setIsSignInInitializationRequestLoading(true);
602609
setError(null);
603610
setMessages([]);
604611

@@ -666,7 +673,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
666673
setError(errorMessage);
667674
onError?.(err as Error);
668675
} finally {
669-
setIsLoading(false);
676+
setIsSignInInitializationRequestLoading(false);
670677
}
671678
};
672679

@@ -686,7 +693,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
686693
touchAllFields();
687694
}
688695

689-
setIsLoading(true);
696+
setIsSignInInitializationRequestLoading(true);
690697
setError(null);
691698
setMessages([]);
692699

@@ -955,7 +962,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
955962
setError(errorMessage);
956963
onError?.(err as Error);
957964
} finally {
958-
setIsLoading(false);
965+
setIsSignInInitializationRequestLoading(false);
959966
}
960967
};
961968

@@ -1020,69 +1027,71 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
10201027

10211028
const errorClasses = clsx([withVendorCSSClassPrefix('signin__error')], errorClassName);
10221029

1023-
const messageClasses = clsx([withVendorCSSClassPrefix('signin__messages')], messageClassName);
1030+
const messageClasses = clsx([withVendorCSSClassPrefix('signin__messages')], messageClassName); // Initialize the flow on component mount
10241031

1025-
// Initialize the flow on component mount
10261032
useEffect(() => {
1027-
if (!isInitialized && !initializationAttemptedRef.current) {
1028-
initializationAttemptedRef.current = true;
1033+
if (isLoading) {
1034+
return;
1035+
}
10291036

1030-
// Inline initialization to avoid dependency issues
1031-
const performInitialization = async () => {
1032-
setIsLoading(true);
1033-
setError(null);
1037+
// React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.
1038+
// https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state
1039+
// This will remount all the useEffects to ensure that there are no unexpected side effects.
1040+
// When react remounts the SignIn, it will send two authorize requests.
1041+
// https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623
1042+
if (reRenderCheckRef.current) {
1043+
return;
1044+
}
10341045

1035-
try {
1036-
const response = await onInitialize();
1046+
reRenderCheckRef.current = true;
10371047

1038-
setCurrentFlow(response);
1039-
setIsInitialized(true);
1040-
onFlowChange?.(response);
1048+
(async () => {
1049+
setIsSignInInitializationRequestLoading(true);
1050+
setError(null);
10411051

1042-
if (response?.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) {
1043-
onSuccess?.((response as any).authData || {});
1044-
return;
1045-
}
1052+
try {
1053+
const response = await onInitialize();
10461054

1047-
if (response?.nextStep?.authenticators?.length > 0) {
1048-
if (
1049-
response.nextStep.stepType === EmbeddedSignInFlowStepType.MultiOptionsPrompt &&
1050-
response.nextStep.authenticators.length > 1
1051-
) {
1052-
setCurrentAuthenticator(null);
1053-
} else {
1054-
const authenticator = response.nextStep.authenticators[0];
1055-
setCurrentAuthenticator(authenticator);
1056-
setupFormFields(authenticator);
1057-
}
1058-
}
1055+
setCurrentFlow(response);
1056+
setIsInitialized(true);
1057+
onFlowChange?.(response);
10591058

1060-
if (response && 'nextStep' in response && response.nextStep && 'messages' in response.nextStep) {
1061-
const stepMessages = (response.nextStep as any).messages || [];
1062-
setMessages(
1063-
stepMessages.map((msg: any) => ({
1064-
type: msg.type || 'INFO',
1065-
message: msg.message || '',
1066-
})),
1067-
);
1068-
}
1069-
} catch (err) {
1070-
const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.in.initialization');
1071-
setError(errorMessage);
1072-
onError?.(err as Error);
1073-
} finally {
1074-
setIsLoading(false);
1059+
if (response?.flowStatus === EmbeddedSignInFlowStatus.SuccessCompleted) {
1060+
onSuccess?.((response as any).authData || {});
1061+
return;
10751062
}
1076-
};
10771063

1078-
performInitialization();
1079-
}
1064+
if (response?.nextStep?.authenticators?.length > 0) {
1065+
if (
1066+
response.nextStep.stepType === EmbeddedSignInFlowStepType.MultiOptionsPrompt &&
1067+
response.nextStep.authenticators.length > 1
1068+
) {
1069+
setCurrentAuthenticator(null);
1070+
} else {
1071+
const authenticator = response.nextStep.authenticators[0];
1072+
setCurrentAuthenticator(authenticator);
1073+
setupFormFields(authenticator);
1074+
}
1075+
}
10801076

1081-
// Cleanup function to reset initialization state on unmount
1082-
return () => {
1083-
initializationAttemptedRef.current = false;
1084-
};
1085-
}, [isInitialized]);
1077+
if (response && 'nextStep' in response && response.nextStep && 'messages' in response.nextStep) {
1078+
const stepMessages = (response.nextStep as any).messages || [];
1079+
setMessages(
1080+
stepMessages.map((msg: any) => ({
1081+
type: msg.type || 'INFO',
1082+
message: msg.message || '',
1083+
})),
1084+
);
1085+
}
1086+
} catch (err) {
1087+
const errorMessage = err instanceof AsgardeoAPIError ? err.message : t('errors.sign.in.initialization');
1088+
setError(errorMessage);
1089+
onError?.(err as Error);
1090+
} finally {
1091+
setIsSignInInitializationRequestLoading(false);
1092+
}
1093+
})();
1094+
}, [isLoading]);
10861095

10871096
if (!isInitialized && isLoading) {
10881097
return (

packages/react/src/components/presentation/SignIn/SignIn.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,24 @@ export interface SignInProps {
7272
* ```
7373
*/
7474
const SignIn: FC<SignInProps> = ({className, size = 'medium', variant = 'outlined'}: SignInProps) => {
75-
const {signIn, afterSignInUrl} = useAsgardeo();
75+
const {signIn, afterSignInUrl, isInitialized, isLoading} = useAsgardeo();
7676

7777
/**
7878
* Initialize the authentication flow.
7979
*/
80-
const handleInitialize = async (): Promise<EmbeddedSignInFlowInitiateResponse> =>
81-
await signIn({response_mode: 'direct'});
80+
const handleInitialize = async (): Promise<EmbeddedSignInFlowInitiateResponse> => {
81+
return await signIn({response_mode: 'direct'});
82+
};
8283

8384
/**
8485
* Handle authentication steps.
8586
*/
8687
const handleOnSubmit = async (
8788
payload: EmbeddedSignInFlowHandleRequestPayload,
8889
request: Request,
89-
): Promise<EmbeddedSignInFlowHandleResponse> => await signIn(payload, request);
90+
): Promise<EmbeddedSignInFlowHandleResponse> => {
91+
return await signIn(payload, request);
92+
};
9093

9194
/**
9295
* Handle successful authentication and redirect with query params.
@@ -107,6 +110,7 @@ const SignIn: FC<SignInProps> = ({className, size = 'medium', variant = 'outline
107110

108111
return (
109112
<BaseSignIn
113+
isLoading={isLoading || !isInitialized}
110114
afterSignInUrl={afterSignInUrl}
111115
onInitialize={handleInitialize}
112116
onSubmit={handleOnSubmit}

0 commit comments

Comments
 (0)